redfetch 1.4.1__tar.gz → 1.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {redfetch-1.4.1 → redfetch-1.4.3}/.github/workflows/pipeline.yml +3 -3
  2. {redfetch-1.4.1 → redfetch-1.4.3}/CHANGELOG.md +10 -0
  3. {redfetch-1.4.1 → redfetch-1.4.3}/PKG-INFO +2 -2
  4. redfetch-1.4.3/pyapp_launcher.py +99 -0
  5. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/__about__.py +2 -2
  6. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/config.py +6 -0
  7. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/config_firstrun.py +25 -1
  8. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/main.py +3 -0
  9. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/processes.py +19 -21
  10. {redfetch-1.4.1 → redfetch-1.4.3}/tests/cli_reference.py +1 -1
  11. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_close_gating.py +44 -2
  12. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_self_heal_eqpath.py +3 -12
  13. redfetch-1.4.1/tests/test_readme_cli_reference.py +0 -36
  14. {redfetch-1.4.1 → redfetch-1.4.3}/.gitignore +0 -0
  15. {redfetch-1.4.1 → redfetch-1.4.3}/AGENTS.md +0 -0
  16. {redfetch-1.4.1 → redfetch-1.4.3}/LICENSE +0 -0
  17. {redfetch-1.4.1 → redfetch-1.4.3}/README.md +0 -0
  18. {redfetch-1.4.1 → redfetch-1.4.3}/pyproject.toml +0 -0
  19. {redfetch-1.4.1 → redfetch-1.4.3}/redfetch.ico +0 -0
  20. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/__init__.py +0 -0
  21. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/api.py +0 -0
  22. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/auth.py +0 -0
  23. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/desktop_shortcut.py +0 -0
  24. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/detecteq.py +0 -0
  25. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/download.py +0 -0
  26. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/listener.py +0 -0
  27. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/meta.py +0 -0
  28. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/navmesh.py +0 -0
  29. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/net.py +0 -0
  30. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/post_update.py +0 -0
  31. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/push.py +0 -0
  32. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/runtime_errors.py +0 -0
  33. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/settings.toml +0 -0
  34. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/shortcuts.py +0 -0
  35. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/special.py +0 -0
  36. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/store.py +0 -0
  37. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync.py +0 -0
  38. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync_discovery.py +0 -0
  39. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync_executor.py +0 -0
  40. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync_planner.py +0 -0
  41. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync_remote.py +0 -0
  42. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/sync_types.py +0 -0
  43. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/terminal_ui.py +0 -0
  44. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/terminal_ui.tcss +0 -0
  45. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/update_status.py +0 -0
  46. {redfetch-1.4.1 → redfetch-1.4.3}/src/redfetch/utils.py +0 -0
  47. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_autologin_eqpath.py +0 -0
  48. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_check.py +0 -0
  49. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_env_selection.py +0 -0
  50. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_fetch_sync_info.py +0 -0
  51. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_first_run_setup.py +0 -0
  52. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_is_special_or_dependency.py +0 -0
  53. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_licensed_resources_filtering.py +0 -0
  54. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_path_reset.py +0 -0
  55. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_process_lock.py +0 -0
  56. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_remote_snapshot.py +0 -0
  57. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_settings_local_deltas.py +0 -0
  58. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_shortcuts.py +0 -0
  59. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_staged_extraction.py +0 -0
  60. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_sync_pipeline_regressions.py +0 -0
  61. {redfetch-1.4.1 → redfetch-1.4.3}/tests/test_sync_queue_rules.py +0 -0
@@ -107,7 +107,7 @@ jobs:
107
107
  id: set_env_vars
108
108
  shell: pwsh
109
109
  run: |
110
- "PYAPP_EXEC_MODULE=redfetch.main" >> $env:GITHUB_ENV
110
+ "PYAPP_EXEC_SCRIPT=$env:GITHUB_WORKSPACE\pyapp_launcher.py" >> $env:GITHUB_ENV
111
111
  "PYAPP_PASS_LOCATION=1" >> $env:GITHUB_ENV
112
112
 
113
113
  if ($env:GITHUB_REF -eq 'refs/heads/develop') {
@@ -165,11 +165,11 @@ jobs:
165
165
  Remove-Item Env:PYAPP_PROJECT_VERSION -ErrorAction SilentlyContinue
166
166
  }
167
167
 
168
- $env:PYAPP_EXEC_MODULE = "${{ env.PYAPP_EXEC_MODULE }}"
168
+ $env:PYAPP_EXEC_SCRIPT = "${{ env.PYAPP_EXEC_SCRIPT }}"
169
169
  $env:PYAPP_PASS_LOCATION = "${{ env.PYAPP_PASS_LOCATION }}"
170
170
  $env:PYAPP_PIP_EXTRA_ARGS = "${{ env.PYAPP_PIP_EXTRA_ARGS }}"
171
171
 
172
- Write-Host "PYAPP_EXEC_MODULE: $env:PYAPP_EXEC_MODULE"
172
+ Write-Host "PYAPP_EXEC_SCRIPT: $env:PYAPP_EXEC_SCRIPT"
173
173
  Write-Host "PYAPP_PASS_LOCATION: $env:PYAPP_PASS_LOCATION"
174
174
  Write-Host "PYAPP_PIP_EXTRA_ARGS: $env:PYAPP_PIP_EXTRA_ARGS"
175
175
 
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.4.3] - 2026-07-06
9
+
10
+ ### Added
11
+ - A failed install will attempt to repair itself, rather than closing immediately.
12
+
13
+ ## [1.4.2] - 2026-07-06
14
+
15
+ ### Fixed
16
+ - The update button wouldn't launch MQ if eqbcs.exe was running.
17
+
8
18
  ## [1.4.1] - 2026-07-05
9
19
 
10
20
  ### Changed
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redfetch
3
- Version: 1.4.1
3
+ Version: 1.4.3
4
4
  Summary: Download and publish EverQuest scripts and software using the RedGuides API
5
5
  Project-URL: Homepage, https://www.redguides.com
6
6
  Project-URL: Documentation, https://www.redguides.com/community/resources/redfetch.3177/
7
7
  Project-URL: Repository, https://github.com/RedGuides/redfetch
8
8
  Project-URL: Issues, https://github.com/RedGuides/redfetch/issues
9
9
  Project-URL: Changelog, https://github.com/RedGuides/redfetch/blob/main/CHANGELOG.md
10
- Project-URL: Source_archive, https://github.com/RedGuides/redfetch/archive/6c3be23e13fb9b1cb6eed3c065b930111c484175.zip
10
+ Project-URL: Source_archive, https://github.com/RedGuides/redfetch/archive/9161a88b101d6df6ce9a697736e3fcc57b1c3122.zip
11
11
  Author-email: Redbot <ask@redguides.com>
12
12
  License-Expression: GPL-3.0-or-later
13
13
  License-File: LICENSE
@@ -0,0 +1,99 @@
1
+ """PyApp entry point for redfetch.exe: run the app, and self-heal a broken install.
2
+
3
+ PyApp treats half-installed environments as complete, so if something fails or
4
+ interrupts install, it stays broken unless someone opens a terminal and runs `self restore`.
5
+ This wrapper runs `self restore` automatically.
6
+
7
+ Embedded via PYAPP_EXEC_SCRIPT. Stdlib-only, and must not import redfetch at module
8
+ scope, since redfetch is exactly what may be missing!
9
+ """
10
+
11
+ import sys
12
+
13
+
14
+ def _import_entrypoint():
15
+ from redfetch.main import main
16
+ return main
17
+
18
+
19
+ # Repair runs after we exit, in a detached script: on Windows `self restore` can't
20
+ # delete the install dir while our python.exe is still running from inside it.
21
+ _REPAIR_BAT = r"""@echo off
22
+ title redfetch - finishing setup
23
+ echo.
24
+ echo redfetch setup didn't finish!
25
+ echo Trying to finish it now, this may take a few minutes.
26
+ echo Please don't close this window.
27
+ echo.
28
+ timeout /t 3 /nobreak > nul
29
+ "<EXE>" self restore
30
+ if %errorlevel% neq 0 goto redfetch_failed
31
+ echo.
32
+ echo All set -- starting redfetch...
33
+ set "REDFETCH_SELFHEAL=1"
34
+ "<EXE>"
35
+ goto redfetch_cleanup
36
+ :redfetch_failed
37
+ echo.
38
+ echo Automatic repair didn't complete. You can repair manually by running:
39
+ echo "<EXE>" self restore
40
+ echo.
41
+ pause
42
+ :redfetch_cleanup
43
+ (goto) 2>nul & del "%~f0"
44
+ """
45
+
46
+
47
+ def _schedule_repair() -> "int | None":
48
+ """Schedule `self restore` + relaunch; return 0 if scheduled, else None."""
49
+ import os
50
+
51
+ # PYAPP is always set (to "1" when PASS_LOCATION is off), so require a real path.
52
+ exe = os.environ.get("PYAPP")
53
+ if sys.platform != "win32" or not exe or not os.path.exists(exe):
54
+ return None
55
+ if os.environ.get("REDFETCH_SELFHEAL"): # already tried once this chain
56
+ return None
57
+
58
+ import subprocess
59
+ import tempfile
60
+
61
+ script = _REPAIR_BAT.replace("<EXE>", exe.replace("%", "%%")) # %% so cmd won't expand the path
62
+ path = None
63
+ try:
64
+ fd, path = tempfile.mkstemp(prefix="redfetch-repair-", suffix=".bat")
65
+ with os.fdopen(fd, "w") as handle:
66
+ handle.write(script)
67
+ subprocess.Popen(
68
+ ["cmd.exe", "/c", path],
69
+ creationflags=subprocess.CREATE_NEW_CONSOLE,
70
+ close_fds=True,
71
+ )
72
+ except Exception:
73
+ if path:
74
+ try:
75
+ os.unlink(path)
76
+ except OSError:
77
+ pass
78
+ return None
79
+ return 0
80
+
81
+
82
+ def main() -> int:
83
+ try:
84
+ entrypoint = _import_entrypoint()
85
+ except ImportError:
86
+ code = _schedule_repair()
87
+ if code is None:
88
+ raise
89
+ print("redfetch needs to finish setting up; opening a repair window...",
90
+ file=sys.stderr)
91
+ return code
92
+
93
+ sys.argv[0] = "redfetch" # so typer's --help shows "redfetch", not this script's path
94
+ entrypoint() # errors past here are app logic, not install problems -> don't self-heal
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__":
99
+ raise SystemExit(main())
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.4.1'
22
- __version_tuple__ = version_tuple = (1, 4, 1)
21
+ __version__ = version = '1.4.3'
22
+ __version_tuple__ = version_tuple = (1, 4, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -493,3 +493,9 @@ def write_env_to_file(new_env):
493
493
  raise RuntimeError("Configuration has not been initialized. Call initialize_config() first.")
494
494
 
495
495
  env_loader.write(env_file_path, {"REDFETCH_ENV": new_env})
496
+
497
+ # Env changed -> re-shows server/config banner.
498
+ try:
499
+ os.remove(os.path.join(os.path.dirname(env_file_path), ".banner_shown"))
500
+ except OSError:
501
+ pass
@@ -4,6 +4,7 @@
4
4
  import os
5
5
  import platform
6
6
  import json
7
+ import time
7
8
 
8
9
  # Third-party
9
10
  from platformdirs import user_config_dir
@@ -21,6 +22,25 @@ from redfetch.detecteq import find_everquest_uninstall_location
21
22
 
22
23
  console = Console()
23
24
 
25
+ # server type and config dir banner
26
+ BANNER_COOLDOWN = 12 * 3600
27
+
28
+
29
+ def _banner_due(config_dir):
30
+ """True unless the banner was shown within the cooldown window."""
31
+ marker = os.path.join(config_dir, ".banner_shown")
32
+ try:
33
+ return (time.time() - os.path.getmtime(marker)) > BANNER_COOLDOWN
34
+ except OSError:
35
+ return True
36
+
37
+
38
+ def _mark_banner_shown(config_dir):
39
+ try:
40
+ open(os.path.join(config_dir, ".banner_shown"), "w").close()
41
+ except OSError:
42
+ pass
43
+
24
44
 
25
45
  # zork-like prompt
26
46
  class CustomPrompt(PromptBase):
@@ -260,7 +280,11 @@ def first_run_setup():
260
280
  if pypi_url_override:
261
281
  notice_lines.append(f"[bold yellow]REDFETCH_PYPI_URL:[/bold yellow] {pypi_url_override}")
262
282
 
263
- console.print(Panel("\n".join(notice_lines), expand=False))
283
+ # Env overrides always spam the status/config banner.
284
+ has_overrides = len(notice_lines) > 2
285
+ if has_overrides or _banner_due(config_dir):
286
+ console.print(Panel("\n".join(notice_lines), expand=False))
287
+ _mark_banner_shown(config_dir)
264
288
  return config_dir
265
289
  else:
266
290
  console.print("[bold red]Environment file (.env) not found. Rerunning setup.[/bold red]")
@@ -539,6 +539,9 @@ def config_show_command(server: Optional[Env] = typer.Option(None, "--server", "
539
539
  if not panel_lines:
540
540
  panel_lines.append("[dim]No paths are currently configured or opted in for this environment.[/dim]")
541
541
 
542
+ # Lead with the server type so users always know which env they're on.
543
+ panel_lines.insert(0, f"[bold yellow]Server type:[/bold yellow] {current_env}")
544
+
542
545
  console.print(Panel("\n".join(panel_lines), expand=False))
543
546
 
544
547
  # Optional: Show full settings dict with a label
@@ -1,6 +1,7 @@
1
1
  """Helpers for managing external processes."""
2
2
  from __future__ import annotations
3
3
 
4
+ import configparser
4
5
  import os
5
6
  import subprocess
6
7
  import sys
@@ -95,22 +96,20 @@ def is_executable_running(exe_path: str, running: set[str] | None = None) -> boo
95
96
 
96
97
 
97
98
  def _spawned_loader_name(mq_folder: str) -> str | None:
98
- """Loader-copy filename from MacroQuest.ini [Internal] SpawnedProcess."""
99
- ini_path = os.path.join(mq_folder, "MacroQuest.ini")
100
- try:
101
- with open(ini_path, encoding="utf-8", errors="ignore") as fh:
102
- in_internal = False
103
- for line in fh:
104
- stripped = line.strip()
105
- if stripped.startswith("["):
106
- in_internal = stripped.lower() == "[internal]"
107
- continue
108
- if in_internal and "=" in stripped:
109
- key, _, value = stripped.partition("=")
110
- if key.strip().lower() == "spawnedprocess":
111
- return value.strip() or None
112
- except OSError:
113
- pass
99
+ """Loader-copy filename from MacroQuest.ini."""
100
+ for ini_path in (
101
+ os.path.join(mq_folder, "config", "MacroQuest.ini"),
102
+ os.path.join(mq_folder, "MacroQuest.ini"),
103
+ ):
104
+
105
+ parser = configparser.ConfigParser(interpolation=None, strict=False)
106
+ try:
107
+ parser.read(ini_path, encoding="utf-8")
108
+ name = parser.get("Internal", "SpawnedProcess", fallback="").strip()
109
+ except (configparser.Error, OSError):
110
+ continue
111
+ if name:
112
+ return name
114
113
  return None
115
114
 
116
115
 
@@ -136,13 +135,12 @@ def macroquest_session_running(mq_folder: str, running: set[str] | None = None)
136
135
  if running is None:
137
136
  running = running_executable_paths()
138
137
  folder = _norm(mq_folder)
138
+ alias = _spawned_loader_name(mq_folder)
139
+ alias = alias.lower() if alias else None
139
140
  for path in running:
140
- if os.path.dirname(path) == folder and os.path.basename(path) != "eqbcs.exe":
141
+ base = os.path.basename(path)
142
+ if base == alias or (base == "macroquest.exe" and os.path.dirname(path) == folder):
141
143
  return True
142
- spawned = _spawned_loader_name(mq_folder)
143
- if spawned:
144
- spawned = spawned.lower()
145
- return any(os.path.basename(path) == spawned for path in running)
146
144
  return False
147
145
 
148
146
 
@@ -5,7 +5,7 @@ changing a command or option, refresh the docs:
5
5
 
6
6
  hatch run dev:gen-docs # or: python tests/cli_reference.py --write
7
7
 
8
- ``test_readme_cli_reference.py`` fails if README.md drifts from this output.
8
+ Run ``hatch run dev:check-docs`` in CI or before release to catch drift.
9
9
  """
10
10
  from __future__ import annotations
11
11
 
@@ -194,10 +194,11 @@ def _norm_set(*paths):
194
194
  return {os.path.normcase(os.path.normpath(p)) for p in paths}
195
195
 
196
196
 
197
- def test_session_running_detects_loader_copy_in_folder(tmp_path, monkeypatch):
197
+ def test_session_ignores_unrecorded_exe_in_folder(tmp_path, monkeypatch):
198
+ # With no loader recorded (fresh install), a stray exe in the folder is NOT a session.
198
199
  monkeypatch.setattr(processes, "_spawned_loader_name", lambda folder: None)
199
200
  running = _norm_set(str(tmp_path / "LV3XGukn.exe"))
200
- assert processes.macroquest_session_running(str(tmp_path), running) is True
201
+ assert processes.macroquest_session_running(str(tmp_path), running) is False
201
202
 
202
203
 
203
204
  @windows_paths
@@ -222,6 +223,39 @@ def test_session_not_running_when_nothing_matches(tmp_path, monkeypatch):
222
223
  assert processes.macroquest_session_running(str(tmp_path), running) is False
223
224
 
224
225
 
226
+ @windows_paths
227
+ def test_session_ignores_renamed_eqbcs_copy_when_loader_not_running(tmp_path, monkeypatch):
228
+ # The reported bug: EQBCS runs as a random-named RunFromTemp copy next to MQ and survives it.
229
+ monkeypatch.setattr(processes, "_spawned_loader_name", lambda folder: "LV3XGukn.exe")
230
+ running = _norm_set(str(tmp_path / "wUUayFYjXM.exe")) # an EQBCS copy; loader not running
231
+ assert processes.macroquest_session_running(str(tmp_path), running) is False
232
+
233
+
234
+ @windows_paths
235
+ def test_session_matches_recorded_loader_amid_other_folder_exes(tmp_path, monkeypatch):
236
+ # The recorded loader name is authoritative: MQ is up when that copy runs, whatever else
237
+ # (LootSort, MeshGenerator, an EQBCS copy...) happens to share the folder.
238
+ monkeypatch.setattr(processes, "_spawned_loader_name", lambda folder: "LV3XGukn.exe")
239
+ running = _norm_set(str(tmp_path / "LV3XGukn.exe"), str(tmp_path / "LootSort.exe"))
240
+ assert processes.macroquest_session_running(str(tmp_path), running) is True
241
+
242
+
243
+ @windows_paths
244
+ def test_session_ignores_folder_tools_when_loader_recorded(tmp_path, monkeypatch):
245
+ # A folder tool running is not a session when a loader name is recorded and its process is gone.
246
+ monkeypatch.setattr(processes, "_spawned_loader_name", lambda folder: "LV3XGukn.exe")
247
+ running = _norm_set(str(tmp_path / "LootSort.exe"))
248
+ assert processes.macroquest_session_running(str(tmp_path), running) is False
249
+
250
+
251
+ @windows_paths
252
+ def test_session_detects_macroquest_exe_direct(tmp_path, monkeypatch):
253
+ # RunFromTemp off: MacroQuest.exe runs directly from the folder -> unambiguously a session.
254
+ monkeypatch.setattr(processes, "_spawned_loader_name", lambda folder: None)
255
+ running = _norm_set(str(tmp_path / "MacroQuest.exe"))
256
+ assert processes.macroquest_session_running(str(tmp_path), running) is True
257
+
258
+
225
259
  def test_spawned_loader_name_parsed_from_ini(tmp_path):
226
260
  (tmp_path / "MacroQuest.ini").write_text(
227
261
  "[Other]\nSpawnedProcess=wrong.exe\n[Internal]\nSpawnedProcess=Ab12Cd34.exe\n"
@@ -230,6 +264,14 @@ def test_spawned_loader_name_parsed_from_ini(tmp_path):
230
264
  assert processes._spawned_loader_name(str(tmp_path / "missing")) is None
231
265
 
232
266
 
267
+ def test_spawned_loader_name_prefers_config_subfolder(tmp_path):
268
+ # Current MQ writes its INI under config/; the reader must look there, and it wins over root.
269
+ (tmp_path / "config").mkdir()
270
+ (tmp_path / "config" / "MacroQuest.ini").write_text("[Internal]\nSpawnedProcess=fromconfig.exe\n")
271
+ (tmp_path / "MacroQuest.ini").write_text("[Internal]\nSpawnedProcess=fromroot.exe\n")
272
+ assert processes._spawned_loader_name(str(tmp_path)) == "fromconfig.exe"
273
+
274
+
233
275
  # --- terminate/restart hardening --------------------------------------------------------
234
276
 
235
277
  def test_terminate_reports_hung_process_and_continues(fake_windows):
@@ -1,9 +1,4 @@
1
- """config.self_heal_eqpath fills a blank/stale EQPATH from MacroQuest autologin at startup.
2
-
3
- Covers the empty-or-invalid heal policy: heal when EQPATH is blank or its folder is gone,
4
- never clobber a valid deliberate value, and keep one env's failure from aborting the others.
5
- The @format linchpin test proves the payoff: a stored EQPATH makes maps resolve to <eq>/maps.
6
- """
1
+ """config.self_heal_eqpath fills a blank/stale EQPATH from MacroQuest autologin at startup."""
7
2
  import os
8
3
  import sqlite3
9
4
 
@@ -138,7 +133,6 @@ def test_no_login_db_no_heal(tmp_path, spy, monkeypatch):
138
133
 
139
134
  assert spy == []
140
135
 
141
-
142
136
  def test_only_blank_env_heals(tmp_path, eq_dir, spy, monkeypatch):
143
137
  vvmq = _make_vvmq(tmp_path, "vvmq_live", "live", eq_dir)
144
138
  envs = {
@@ -169,11 +163,7 @@ def test_exception_in_one_env_does_not_abort_others(tmp_path, eq_dir, spy, monke
169
163
  # --- the @format linchpin ---------------------------------------------------
170
164
 
171
165
  def test_maps_resolve_under_stored_eqpath(tmp_path, monkeypatch):
172
- """A stored EQPATH (where the heal writes it) makes maps resource 153 resolve to <eq>/maps.
173
-
174
- This is the whole point of self-heal over the old read-through: fix the stored value once
175
- and every `@format {this.eqpath}` consumer lights up for free.
176
- """
166
+ """A stored EQPATH (where the heal writes it) makes maps resource 153 resolve to <eq>/maps."""
177
167
  from dynaconf import Dynaconf
178
168
  from redfetch import sync_discovery
179
169
 
@@ -196,3 +186,4 @@ def test_maps_resolve_under_stored_eqpath(tmp_path, monkeypatch):
196
186
 
197
187
  resolved = sync_discovery.resolve_root_path("153", None, "LIVE")
198
188
  assert resolved == os.path.normpath(str(eq / "maps"))
189
+
@@ -1,36 +0,0 @@
1
- """Guard against README drift: the Command Line Reference is generated from the
2
- typer app (see cli_reference.py). This fails if README.md is out of date, i.e.
3
- someone changed a command/option in main.py but didn't regenerate the docs.
4
-
5
- Fix a failure with: hatch run dev:gen-docs
6
- """
7
- import difflib
8
- import sys
9
- from pathlib import Path
10
-
11
- sys.path.insert(0, str(Path(__file__).resolve().parent)) # import the sibling helper
12
-
13
- from cli_reference import README, current_block, render_reference
14
-
15
-
16
- def test_readme_cli_reference_is_current():
17
- readme = README.read_text(encoding="utf-8")
18
- block = current_block(readme)
19
- assert block is not None, (
20
- "CLI reference markers are missing from README.md. Restore them and run "
21
- "`hatch run dev:gen-docs`."
22
- )
23
-
24
- expected = render_reference()
25
- if block != expected:
26
- diff = "\n".join(
27
- difflib.unified_diff(
28
- block.splitlines(), expected.splitlines(),
29
- fromfile="README.md (committed)", tofile="generated from main.py",
30
- lineterm="",
31
- )
32
- )
33
- raise AssertionError(
34
- "README.md Command Line Reference is stale. Regenerate with "
35
- "`hatch run dev:gen-docs` (command help lives in src/redfetch/main.py).\n" + diff
36
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes