diff --git a/README.md b/README.md index 354af0b..6ee7e99 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,11 @@ sudo python qoder_reset_gui.py - Check if Qoder is running in the background - Verify application data directory exists +**Issue**: "Only the lightweight model works, but other models cannot be used" +- This usually means login/provider routing settings were cleared. Avoid running **Clean Login Identity** unless you intend to sign in again. +- In the GUI, keep **Preserve login/model settings (recommended)** enabled when doing deep cleanup. +- If you already cleaned login identity: restart Qoder and sign in again (or reinstall Qoder if sign-in state can’t be recovered). + ## 📄 License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/qoder_reset_gui.py b/qoder_reset_gui.py index 7a7f861..cbd0523 100644 --- a/qoder_reset_gui.py +++ b/qoder_reset_gui.py @@ -18,15 +18,169 @@ import random from pathlib import Path from datetime import datetime, timedelta +from typing import Callable, Mapping, Optional, Tuple try: from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * except ImportError: - print("Error: PyQt5 is not installed") - print("Please run: pip install PyQt5") - sys.exit(1) + # Allow importing this module in headless / test environments where PyQt5 + # isn't available. The GUI entrypoint will still error clearly when run. + PYQT_AVAILABLE = False + QMainWindow = object # type: ignore +else: + PYQT_AVAILABLE = True + + +def _sharedclientcache_files_to_delete(*, preserve_model_settings: bool) -> tuple[str, ...]: + """ + Return SharedClientCache files that are safe to delete. + + `mcp.json` is treated as model/provider routing/config and is preserved by + default to avoid breaking non-lightweight models. + """ + files = [".info", ".lock"] + if not preserve_model_settings or os.environ.get("QODER_CLEAN_MCP_JSON") == "1": + files.append("mcp.json") + return tuple(files) + + +def resolve_qoder_data_dir( + system: Optional[str] = None, + env: Optional[Mapping[str, str]] = None, + home_dir: Optional[Path] = None, +) -> Path: + system = system or platform.system() + env = env or os.environ + home_dir = home_dir or Path.home() + + if system == "Windows": + # Prefer %APPDATA% when available; home/AppData/Roaming is only a fallback. + appdata = env.get("APPDATA") + if appdata: + return Path(appdata) / "Qoder" + return home_dir / "AppData" / "Roaming" / "Qoder" + + if system == "Linux": + xdg_config_home = env.get("XDG_CONFIG_HOME") + base = Path(xdg_config_home) if xdg_config_home else (home_dir / ".config") + return base / "Qoder" + + # macOS (and default fallback) + return home_dir / "Library" / "Application Support" / "Qoder" + + +def _qoder_platform_value(system: Optional[str] = None) -> str: + system = system or platform.system() + if system == "Windows": + return "win32" + if system == "Linux": + return "linux" + return "darwin" + + +def kill_qoder_process( + system: Optional[str] = None, + run: Callable[..., subprocess.CompletedProcess] = subprocess.run, +) -> Tuple[bool, str]: + """ + Best-effort attempt to terminate Qoder. + Returns (success, details) where details is stdout/stderr text when available. + """ + system = system or platform.system() + + if system == "Windows": + result = run( + ["taskkill", "/F", "/T", "/IM", "qoder.exe"], + capture_output=True, + text=True, + ) + elif system == "Darwin": + result = run(["pkill", "-x", "Qoder"], capture_output=True, text=True) + elif system == "Linux": + result = run(["pkill", "-x", "qoder"], capture_output=True, text=True) + else: + return False, f"Unsupported platform: {system}" + + details = "" + if getattr(result, "stdout", None): + details += result.stdout.strip() + if getattr(result, "stderr", None): + if details: + details += "\n" + details += result.stderr.strip() + + # On Windows taskkill returns non-zero when the process isn't found; treat that as not fatal. + if system == "Windows" and "not found" in details.lower(): + return True, details + + return result.returncode == 0, details + + +def reset_qoder_machine_id(qoder_support_dir: Path) -> str: + if not qoder_support_dir.exists(): + raise FileNotFoundError(f"Qoder data directory not found: {qoder_support_dir}") + + new_machine_id = str(uuid.uuid4()) + machine_id_file = qoder_support_dir / "machineid" + machine_id_file.write_text(new_machine_id, encoding="utf-8") + return new_machine_id + + +def reset_qoder_telemetry(qoder_support_dir: Path, system: Optional[str] = None) -> Mapping[str, str]: + if not qoder_support_dir.exists(): + raise FileNotFoundError(f"Qoder data directory not found: {qoder_support_dir}") + + storage_json_file = qoder_support_dir / "User" / "globalStorage" / "storage.json" + storage_json_file.parent.mkdir(parents=True, exist_ok=True) + + if storage_json_file.exists(): + try: + data = json.loads(storage_json_file.read_text(encoding="utf-8")) + except Exception: + data = {} + else: + data = {} + + new_uuid = str(uuid.uuid4()) + machine_id_hash = hashlib.sha256(new_uuid.encode()).hexdigest() + device_id = str(uuid.uuid4()) + sqm_id = str(uuid.uuid4()) + session_id = str(uuid.uuid4()) + installation_id = str(uuid.uuid4()) + + data["telemetry.machineId"] = machine_id_hash + data["telemetry.devDeviceId"] = device_id + data["telemetry.sqmId"] = sqm_id + data["telemetry.sessionId"] = session_id + data["telemetry.installationId"] = installation_id + + # Additional identifiers commonly used as fallbacks. + data["telemetry.clientId"] = str(uuid.uuid4()) + data["telemetry.userId"] = str(uuid.uuid4()) + data["telemetry.anonymousId"] = str(uuid.uuid4()) + data["machineId"] = machine_id_hash + data["deviceId"] = device_id + data["installationId"] = str(uuid.uuid4()) + data["hardwareId"] = str(uuid.uuid4()) + data["platformId"] = str(uuid.uuid4()) + + data["system.platform"] = _qoder_platform_value(system) + data["system.arch"] = platform.machine() + + storage_json_file.write_text( + json.dumps(data, indent=4, ensure_ascii=False), + encoding="utf-8", + ) + + return { + "telemetry.machineId": machine_id_hash, + "telemetry.devDeviceId": device_id, + "telemetry.sqmId": sqm_id, + "telemetry.sessionId": session_id, + "telemetry.installationId": installation_id, + } def _configure_qt_runtime(): """ @@ -34,6 +188,9 @@ def _configure_qt_runtime(): Helps avoid: "This application failed to start because no Qt platform plugin could be initialized" """ + if not globals().get("PYQT_AVAILABLE", False): + return {"plugins_dir": None, "platforms_dir": None, "frozen": False} + # Some environments carry a broken/mismatched QT_PLUGIN_PATH that breaks plugin discovery. # Prefer the PyQt5-bundled plugins path (or PyInstaller bundle) when available. env_keys = ( @@ -150,6 +307,7 @@ def init_translations(self): 'hardware_fingerprint_reset': '硬件指纹重置', 'advanced_options': '高级选项', 'preserve_chat': '保留对话记录', + 'preserve_model_settings': '保留登录/模型设置(推荐)', 'operation_log': '操作日志:', 'clear_log': '清空日志', 'github': 'Github', @@ -195,6 +353,7 @@ def init_translations(self): 'hardware_fingerprint_reset': 'Hardware Fingerprint Reset', 'advanced_options': 'Advanced Options', 'preserve_chat': 'Preserve Chat History', + 'preserve_model_settings': 'Preserve login/model settings (recommended)', 'operation_log': 'Operation Log:', 'clear_log': 'Clear Log', 'github': 'Github', @@ -240,6 +399,7 @@ def init_translations(self): 'hardware_fingerprint_reset': 'Сброс железа', 'advanced_options': 'Дополнительно', 'preserve_chat': 'Сохранить чат', + 'preserve_model_settings': 'Сохранить вход/настройки моделей (рекомендуется)', 'operation_log': 'Журнал операций:', 'clear_log': 'Очистить журнал', 'github': 'Github', @@ -285,6 +445,7 @@ def init_translations(self): 'hardware_fingerprint_reset': 'Reset de Hardware', 'advanced_options': 'Opções Avançadas', 'preserve_chat': 'Preservar Histórico do chat', + 'preserve_model_settings': 'Preservar login/configuração de modelos (recomendado)', 'operation_log': 'Log de Operações:', 'clear_log': 'Limpar Log', 'github': 'Github', @@ -330,6 +491,7 @@ def init_translations(self): 'hardware_fingerprint_reset': 'Đặt Lại Dấu Vân Tay Phần Cứng', 'advanced_options': 'Tùy Chọn Nâng Cao', 'preserve_chat': 'Giữ Lại Lịch Sử Trò Chuyện', + 'preserve_model_settings': 'Giữ đăng nhập/cấu hình model (khuyến nghị)', 'operation_log': 'Nhật Ký Thao Tác:', 'clear_log': 'Xóa Nhật Ký', 'github': 'Liên Kết GitHub', @@ -500,6 +662,20 @@ def init_ui(self): } """) main_layout.addWidget(self.preserve_chat_checkbox) + + # Preserve login/model settings checkbox (prevents breaking non-lightweight models) + self.preserve_model_settings_checkbox = QCheckBox(self.tr('preserve_model_settings')) + self.preserve_model_settings_checkbox.setChecked(True) + self.preserve_model_settings_checkbox.setStyleSheet(""" + QCheckBox { + spacing: 8px; + } + QCheckBox::indicator { + width: 18px; + height: 18px; + } + """) + main_layout.addWidget(self.preserve_model_settings_checkbox) # Log Area log_layout = QVBoxLayout() @@ -616,6 +792,7 @@ def change_language(self, language_text): # Update checkbox self.preserve_chat_checkbox.setText(self.tr('preserve_chat')) + self.preserve_model_settings_checkbox.setText(self.tr('preserve_model_settings')) # Optional: log the language change self.log(f"Language changed to: {language_text}") @@ -655,6 +832,7 @@ def update_ui_text(self): # 更新复选框文本 self.preserve_chat_checkbox.setText(self.tr('preserve_chat')) + self.preserve_model_settings_checkbox.setText(self.tr('preserve_model_settings')) # 清空日志并重新初始化 self.log_text.clear() @@ -762,15 +940,7 @@ def clear_log(self): def get_qoder_data_dir(self): """Get Qoder data directory path (cross-platform support)""" - home_dir = Path.home() - system = platform.system() - - if system == "Windows": - # Windows: %APPDATA%\Qoder - return home_dir / "AppData" / "Roaming" / "Qoder" - else: - # Default to macOS path as fallback - return home_dir / "Library" / "Application Support" / "Qoder" + return resolve_qoder_data_dir() def is_qoder_running(self): """Check if Qoder is currently running""" @@ -849,12 +1019,18 @@ def close_qoder(self): if reply == QMessageBox.Yes: # Execute Qoder closing operation self.log("Closing Qoder...") - - # Prompt successful closure + ok, details = kill_qoder_process() + if ok: + self.log("✅ Qoder process terminated.") + else: + self.log("⚠️ Failed to terminate Qoder process.") + if details: + self.log(details) + QMessageBox.information( - self, - self.tr('success'), - "Qoder has been closed successfully." + self, + self.tr("success") if ok else self.tr("warning"), + "Qoder has been closed successfully." if ok else "Failed to close Qoder.", ) except Exception as e: # Log error @@ -868,6 +1044,15 @@ def close_qoder(self): def login_identity_cleanup(self): """Clean login-related identity information""" try: + reply = QMessageBox.question( + self, + self.tr('warning'), + "This will log you out and may cause non-lightweight models to stop working until you sign in again.\n\nContinue?", + QMessageBox.Yes | QMessageBox.No, + ) + if reply != QMessageBox.Yes: + return + # Clean critical login-related files qoder_support_dir = self.get_qoder_data_dir() @@ -929,8 +1114,14 @@ def reset_telemetry(self): if reply == QMessageBox.Yes: # Execute telemetry reset operation self.log("Resetting Telemetry data...") - - # Prompt reset success + qoder_support_dir = self.get_qoder_data_dir() + updated = reset_qoder_telemetry(qoder_support_dir) + self.log( + f" New Telemetry Machine ID: {updated['telemetry.machineId'][:16]}..." + ) + self.log(f" New Device ID: {updated['telemetry.devDeviceId']}") + self.log(f" New SQM ID: {updated['telemetry.sqmId']}") + QMessageBox.information( self, self.tr('success'), @@ -969,8 +1160,10 @@ def reset_machine_id(self): if reply == QMessageBox.Yes: # Execute machine ID reset operation self.log("Resetting Machine ID...") - - # Prompt reset success + qoder_support_dir = self.get_qoder_data_dir() + new_machine_id = reset_qoder_machine_id(qoder_support_dir) + self.log(f" New Machine ID: {new_machine_id}") + QMessageBox.information( self, self.tr('success'), @@ -1007,8 +1200,20 @@ def deep_identity_cleanup(self): ) if reply == QMessageBox.Yes: + preserve_chat = self.preserve_chat_checkbox.isChecked() + preserve_model_settings = self.preserve_model_settings_checkbox.isChecked() + # Execute deep identity cleanup self.log("Performing deep identity cleanup...") + qoder_support_dir = self.get_qoder_data_dir() + if not qoder_support_dir.exists(): + raise Exception("Qoder application data directory not found") + + self.perform_advanced_identity_cleanup( + qoder_support_dir, + preserve_chat=preserve_chat, + preserve_model_settings=preserve_model_settings, + ) # Prompt cleanup success QMessageBox.information( @@ -1068,15 +1273,30 @@ def hardware_fingerprint_reset(self): def one_click_reset(self): """一键修改所有配置""" try: - # 检查Qoder是否在运行 + # If Qoder is running, offer to close it automatically so we can patch its data. if self.is_qoder_running(): - QMessageBox.warning( - self, - self.tr('warning'), - self.tr('qoder_detected_running') + "\n" + - self.tr('please_close_qoder') + reply = QMessageBox.question( + self, + self.tr("confirm_close_qoder"), + self.tr("qoder_detected_running") + + "\n" + + self.tr("please_close_qoder") + + "\n\nClose Qoder now and continue?", + QMessageBox.Yes | QMessageBox.No, ) - return + if reply != QMessageBox.Yes: + return + + ok, details = kill_qoder_process() + if details: + self.log(details) + if not ok and self.is_qoder_running(): + QMessageBox.warning( + self, + self.tr("warning"), + "Failed to close Qoder. Please close it manually and retry.", + ) + return # 确认操作 reply = QMessageBox.question( @@ -1088,18 +1308,14 @@ def one_click_reset(self): if reply == QMessageBox.Yes: preserve_chat = self.preserve_chat_checkbox.isChecked() + preserve_model_settings = self.preserve_model_settings_checkbox.isChecked() # 执行重置操作 self.log("Performing one-click reset...") - - # 关闭Qoder - self.close_qoder() - - # 重置机器ID - self.reset_machine_id() - - # 重置遥测数据 - self.reset_telemetry() + self.perform_full_reset( + preserve_chat=preserve_chat, + preserve_model_settings=preserve_model_settings, + ) # 提示操作完成 QMessageBox.information( @@ -1118,7 +1334,7 @@ def one_click_reset(self): f"An error occurred: {str(e)}" ) - def perform_full_reset(self, preserve_chat=True): + def perform_full_reset(self, preserve_chat=True, preserve_model_settings=True): """执行完整重置""" qoder_support_dir = self.get_qoder_data_dir() @@ -1127,13 +1343,8 @@ def perform_full_reset(self, preserve_chat=True): # 1. 重置机器ID(增强版) self.log("1. 重置机器ID...") - # 主机器ID文件 - machine_id_file = qoder_support_dir / "machineid" - if machine_id_file.exists() or True: # 总是创建 - new_machine_id = str(uuid.uuid4()) - with open(machine_id_file, 'w') as f: - f.write(new_machine_id) - self.log(" 主机器ID已重置") + reset_qoder_machine_id(qoder_support_dir) + self.log(" 主机器ID已重置") # 增强:创建多个可能的机器ID文件 additional_id_files = [ @@ -1149,71 +1360,64 @@ def perform_full_reset(self, preserve_chat=True): # 2. 重置遥测数据 self.log("2. 重置遥测数据...") + updated = reset_qoder_telemetry(qoder_support_dir) storage_json_file = qoder_support_dir / "User/globalStorage/storage.json" - if storage_json_file.exists(): - with open(storage_json_file, 'r', encoding='utf-8') as f: - data = json.load(f) - - new_uuid = str(uuid.uuid4()) - machine_id_hash = hashlib.sha256(new_uuid.encode()).hexdigest() - device_id = str(uuid.uuid4()) - sqm_id = str(uuid.uuid4()) # 新增:软件质量度量ID - - # 重置所有遥测相关的标识符(增强版) - data['telemetry.machineId'] = machine_id_hash - data['telemetry.devDeviceId'] = device_id - data['telemetry.sqmId'] = sqm_id - - # 新增:重置更多可能的硬件指纹标识符 - data['telemetry.sessionId'] = str(uuid.uuid4()) - data['telemetry.installationId'] = str(uuid.uuid4()) - data['telemetry.clientId'] = str(uuid.uuid4()) - data['telemetry.userId'] = str(uuid.uuid4()) - data['telemetry.anonymousId'] = str(uuid.uuid4()) - data['machineId'] = machine_id_hash # 备用机器ID - data['deviceId'] = device_id # 备用设备ID - data['installationId'] = str(uuid.uuid4()) # 安装ID - data['hardwareId'] = str(uuid.uuid4()) # 硬件ID - data['platformId'] = str(uuid.uuid4()) # 平台ID - - # 重置系统指纹相关配置 - data['system.platform'] = 'darwin' # 保持平台一致但重置其他 - data['system.arch'] = platform.machine() # 重置架构信息 - data['system.version'] = f"{random.randint(10, 15)}.{random.randint(0, 9)}.{random.randint(0, 9)}" - - self.log(f" 新会话ID: {data['telemetry.sessionId'][:16]}...") - self.log(f" 新安装ID: {data['telemetry.installationId'][:16]}...") - self.log(f" 新硬件ID: {data['hardwareId'][:16]}...") - - # 清除其他可能的身份识别配置(保留对话时不清除) - if not preserve_chat: - # 完全重置模式:清除所有可能的身份相关配置 - identity_keys_to_remove = [] - for key in data.keys(): - if any(keyword in key.lower() for keyword in [ - 'auth', 'login', 'session', 'token', 'credential', - 'device', 'fingerprint', 'tracking', 'analytics' - ]): - identity_keys_to_remove.append(key) - - for key in identity_keys_to_remove: - del data[key] - self.log(f" 已清除配置: {key}") + with open(storage_json_file, "r", encoding="utf-8") as f: + data = json.load(f) + + # Ensure system fingerprint fields are consistent with the current platform. + data["system.platform"] = _qoder_platform_value() + data["system.arch"] = platform.machine() + data["system.version"] = self.generate_system_version(platform.system()) + # Optional identity cleanup: avoid deleting telemetry keys we just wrote. + if not preserve_chat: + if preserve_model_settings: + identity_keywords = ("fingerprint", "tracking", "analytics") else: - # 保留对话模式:只清除明确的身份识别配置 - self.log(" 保留对话模式:保留非身份相关配置") + identity_keywords = ( + "auth", + "login", + "session", + "token", + "credential", + "fingerprint", + "tracking", + "analytics", + ) + protected_prefixes = ("telemetry.",) + protected_keys = { + "machineId", + "deviceId", + "installationId", + "hardwareId", + "platformId", + } + + identity_keys_to_remove = [] + for key in list(data.keys()): + key_lower = str(key).lower() + if key in protected_keys or key_lower.startswith(protected_prefixes): + continue + if any(keyword in key_lower for keyword in identity_keywords): + identity_keys_to_remove.append(key) + + for key in identity_keys_to_remove: + data.pop(key, None) + self.log(f" 已清除配置: {key}") + else: + self.log(" 保留对话模式:保留非身份相关配置") - with open(storage_json_file, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=4, ensure_ascii=False) + with open(storage_json_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4, ensure_ascii=False) - self.log(f" 新遥测机器ID: {machine_id_hash[:16]}...") - self.log(f" 新设备ID: {device_id}") - self.log(f" 新SQM ID: {sqm_id}") + self.log(f" 新遥测机器ID: {updated['telemetry.machineId'][:16]}...") + self.log(f" 新设备ID: {updated['telemetry.devDeviceId']}") + self.log(f" 新SQM ID: {updated['telemetry.sqmId']}") # 3. 清理缓存(增强版) self.log("3. 清理缓存数据...") cache_dirs = [ - "Cache", "blob_storage", "Code Cache", "SharedClientCache", + "Cache", "blob_storage", "Code Cache", "GPUCache", "DawnGraphiteCache", "DawnWebGPUCache", # 新增:更多可能包含指纹的缓存 "ShaderCache", "DawnCache", "Dictionaries", @@ -1231,6 +1435,17 @@ def perform_full_reset(self, preserve_chat=True): except: pass + # SharedClientCache can contain model/provider routing config (e.g. mcp.json). + # Preserve by default to avoid breaking access to other models. + if not preserve_model_settings: + shared_client_cache = qoder_support_dir / "SharedClientCache" + if shared_client_cache.exists(): + try: + shutil.rmtree(shared_client_cache) + cleaned += 1 + except Exception: + pass + self.log(f" 已清理 {cleaned} 个缓存目录") # 4. 清理身份识别文件(增强版) @@ -1318,11 +1533,18 @@ def perform_full_reset(self, preserve_chat=True): # 5. 执行高级身份清理(新增) self.log("5. 执行高级身份清理...") - self.perform_advanced_identity_cleanup(qoder_support_dir, preserve_chat) + self.perform_advanced_identity_cleanup( + qoder_support_dir, + preserve_chat=preserve_chat, + preserve_model_settings=preserve_model_settings, + ) # 6. 执行登录身份清理(新增 - 清理登录状态) - self.log("6. 执行登录身份清理...") - self.perform_login_identity_cleanup(qoder_support_dir) + if not preserve_model_settings: + self.log("6. 执行登录身份清理...") + self.perform_login_identity_cleanup(qoder_support_dir) + else: + self.log("6. 跳过登录身份清理(保留登录/模型设置)") # 7. 执行硬件指纹重置(新增 - 最强反检测) self.log("7. 执行硬件指纹重置...") @@ -1340,7 +1562,12 @@ def perform_full_reset(self, preserve_chat=True): self.log("9. 清除对话记录...") self.clear_chat_history(qoder_support_dir) - def perform_advanced_identity_cleanup(self, qoder_support_dir, preserve_chat=False): + def perform_advanced_identity_cleanup( + self, + qoder_support_dir, + preserve_chat=False, + preserve_model_settings=True, + ): """执行高级身份清理,清除所有可能的身份识别信息""" try: self.log("开始高级身份清理...") @@ -1350,8 +1577,9 @@ def perform_advanced_identity_cleanup(self, qoder_support_dir, preserve_chat=Fal shared_cache = qoder_support_dir / "SharedClientCache" if shared_cache.exists(): # 总是清理这些关键的身份文件(会重新生成) - critical_files = [".info", ".lock", "mcp.json"] - for file_name in critical_files: + for file_name in _sharedclientcache_files_to_delete( + preserve_model_settings=preserve_model_settings + ): file_path = shared_cache / file_name if file_path.exists(): try: @@ -2010,6 +2238,11 @@ def perform_hardware_fingerprint_reset(self, qoder_support_dir): raise def main(): + if not globals().get("PYQT_AVAILABLE", False): + print("Error: PyQt5 is not installed") + print("Please run: pip install -r requirements.txt") + raise SystemExit(1) + _configure_qt_runtime() app = QApplication(sys.argv) diff --git a/test_preserve_model_settings.py b/test_preserve_model_settings.py new file mode 100644 index 0000000..1a40ddb --- /dev/null +++ b/test_preserve_model_settings.py @@ -0,0 +1,33 @@ +import os + + +def test_sharedclientcache_files_to_delete_preserves_mcp_by_default(): + import qoder_reset_gui + + os.environ.pop("QODER_CLEAN_MCP_JSON", None) + files = qoder_reset_gui._sharedclientcache_files_to_delete(preserve_model_settings=True) + assert "mcp.json" not in files + + +def test_sharedclientcache_files_to_delete_can_delete_mcp_when_requested(): + import qoder_reset_gui + + os.environ.pop("QODER_CLEAN_MCP_JSON", None) + files = qoder_reset_gui._sharedclientcache_files_to_delete(preserve_model_settings=False) + assert "mcp.json" in files + + +def test_sharedclientcache_files_to_delete_env_override(): + import qoder_reset_gui + + old = os.environ.get("QODER_CLEAN_MCP_JSON") + try: + os.environ["QODER_CLEAN_MCP_JSON"] = "1" + files = qoder_reset_gui._sharedclientcache_files_to_delete(preserve_model_settings=True) + assert "mcp.json" in files + finally: + if old is None: + os.environ.pop("QODER_CLEAN_MCP_JSON", None) + else: + os.environ["QODER_CLEAN_MCP_JSON"] = old + diff --git a/test_reset_helpers.py b/test_reset_helpers.py new file mode 100644 index 0000000..d43c849 --- /dev/null +++ b/test_reset_helpers.py @@ -0,0 +1,42 @@ +import json +import uuid +from pathlib import Path + +from qoder_reset_gui import resolve_qoder_data_dir, reset_qoder_machine_id, reset_qoder_telemetry + + +def test_resolve_qoder_data_dir_windows_prefers_appdata(tmp_path): + home_dir = tmp_path / "home" + appdata = tmp_path / "AppData" / "Roaming" + expected = appdata / "Qoder" + + resolved = resolve_qoder_data_dir( + system="Windows", + env={"APPDATA": str(appdata)}, + home_dir=home_dir, + ) + assert resolved == expected + + +def test_reset_qoder_machine_id_writes_machineid(tmp_path): + new_machine_id = reset_qoder_machine_id(tmp_path) + assert (tmp_path / "machineid").is_file() + assert (tmp_path / "machineid").read_text(encoding="utf-8") == new_machine_id + uuid.UUID(new_machine_id) + + +def test_reset_qoder_telemetry_creates_and_updates_storage_json(tmp_path): + storage_json_file = tmp_path / "User" / "globalStorage" / "storage.json" + storage_json_file.parent.mkdir(parents=True, exist_ok=True) + storage_json_file.write_text(json.dumps({"foo": "bar"}), encoding="utf-8") + + updated = reset_qoder_telemetry(tmp_path, system="Linux") + assert storage_json_file.is_file() + + data = json.loads(storage_json_file.read_text(encoding="utf-8")) + assert data["foo"] == "bar" + assert data["telemetry.machineId"] == updated["telemetry.machineId"] + assert data["telemetry.devDeviceId"] == updated["telemetry.devDeviceId"] + assert data["telemetry.sqmId"] == updated["telemetry.sqmId"] + assert data["system.platform"] == "linux" +