witbitz-code 1.6.2__tar.gz → 1.6.4__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 (100) hide show
  1. {witbitz_code-1.6.2/src/witbitz_code.egg-info → witbitz_code-1.6.4}/PKG-INFO +1 -1
  2. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/pyproject.toml +1 -1
  3. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/__init__.py +1 -1
  4. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/auto.py +23 -1
  5. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/auto_runner.py +4 -3
  6. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/ui.py +6 -2
  7. {witbitz_code-1.6.2 → witbitz_code-1.6.4/src/witbitz_code.egg-info}/PKG-INFO +1 -1
  8. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_auto.py +49 -0
  9. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_ui.py +13 -0
  10. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/LICENSE +0 -0
  11. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/README.md +0 -0
  12. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/setup.cfg +0 -0
  13. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/__main__.py +0 -0
  14. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/_js.py +0 -0
  15. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/account.py +0 -0
  16. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/asks.py +0 -0
  17. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/attachments.py +0 -0
  18. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/cli.py +0 -0
  19. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/codekeys.py +0 -0
  20. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/codetools.py +0 -0
  21. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/compress.py +0 -0
  22. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/computers.py +0 -0
  23. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/confidential.py +0 -0
  24. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/confidential_models.json +0 -0
  25. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/connector.py +0 -0
  26. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/devicelink.py +0 -0
  27. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/dirs.py +0 -0
  28. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/folders.py +0 -0
  29. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/gateway.py +0 -0
  30. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
  31. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/icons/witbitz-code.ico +0 -0
  32. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/keys.py +0 -0
  33. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/link.py +0 -0
  34. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/mkdir.py +0 -0
  35. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/models.py +0 -0
  36. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/net.py +0 -0
  37. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/offered_models.json +0 -0
  38. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  39. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  40. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  41. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  42. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/outputs.py +0 -0
  43. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/pair.py +0 -0
  44. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/pairings.py +0 -0
  45. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/payers.py +0 -0
  46. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/plugins.py +0 -0
  47. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/policy.py +0 -0
  48. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/ports.py +0 -0
  49. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/qr.py +0 -0
  50. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/receipt.py +0 -0
  51. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/relay.py +0 -0
  52. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/seen.py +0 -0
  53. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/tinfoil_read.py +0 -0
  54. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/tools_catalog.json +0 -0
  55. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/tools_probe.py +0 -0
  56. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/toolsetup.py +0 -0
  57. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/ui_static/app.css +0 -0
  58. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/ui_static/app.js +0 -0
  59. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/ui_static/index.html +0 -0
  60. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/uitools.py +0 -0
  61. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/uninstall.py +0 -0
  62. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/winagent.py +0 -0
  63. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/winbg.py +0 -0
  64. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/winconsole.py +0 -0
  65. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/wizard.py +0 -0
  66. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code/wslstart.py +0 -0
  67. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
  68. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  69. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  70. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code.egg-info/requires.txt +0 -0
  71. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/src/witbitz_code.egg-info/top_level.txt +0 -0
  72. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_asks.py +0 -0
  73. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_attachments.py +0 -0
  74. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_cli.py +0 -0
  75. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_codekeys.py +0 -0
  76. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_codetools.py +0 -0
  77. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_confidential.py +0 -0
  78. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_connector_e2e.py +0 -0
  79. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_devicelink.py +0 -0
  80. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_dirs.py +0 -0
  81. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_files_and_compress.py +0 -0
  82. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_folders.py +0 -0
  83. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_outputs.py +0 -0
  84. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_pair_flows.py +0 -0
  85. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_plugins.py +0 -0
  86. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_policy.py +0 -0
  87. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_ports.py +0 -0
  88. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_ports_win.py +0 -0
  89. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_registry.py +0 -0
  90. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_relay_codec.py +0 -0
  91. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_setup.py +0 -0
  92. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_tools_probe.py +0 -0
  93. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_toolsetup.py +0 -0
  94. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_uitools.py +0 -0
  95. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_uninstall.py +0 -0
  96. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_winagent.py +0 -0
  97. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_winbg.py +0 -0
  98. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_winconsole.py +0 -0
  99. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_windows_files.py +0 -0
  100. {witbitz_code-1.6.2 → witbitz_code-1.6.4}/tests/test_wslstart.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.6.2
3
+ Version: 1.6.4
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "witbitz-code"
7
- version = "1.6.2"
7
+ version = "1.6.4"
8
8
  description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -4,4 +4,4 @@ The Python implementation of tools/witbitz-code.mjs (docs/opencode-relay.md phas
4
4
  vectors and cross-implementation tests.
5
5
  """
6
6
 
7
- __version__ = "1.6.2"
7
+ __version__ = "1.6.4"
@@ -206,6 +206,25 @@ _KEY_FILE = re.compile(r"\.(pem|key|p12|pfx|jks|keystore)\Z", re.ASCII | re.IGNO
206
206
  _SECRETISH = re.compile(r"secret|credential", re.ASCII | re.IGNORECASE)
207
207
 
208
208
 
209
+ _WIN_ABS = re.compile(r"\A(?:\\\\\?\\)?([A-Za-z]):[\\/]")
210
+
211
+
212
+ def windows_to_posix(path: Any) -> Any:
213
+ """A Windows path in the form every rule here reads: C:\\Users\\x, C:/Users/x → /c/Users/x (Git Bash's own spelling).
214
+ Every rule tests "absolute" as a leading / — so on Windows (the owner, 2026-10-09: "The auto mode is not working
215
+ (windows)") the project folder was never absolute and Auto would not switch on, and C:/… in a command read as a path
216
+ INSIDE the project. Anything else is returned as it came."""
217
+ if not isinstance(path, str):
218
+ return path
219
+ m = _WIN_ABS.match(path)
220
+ return "/" + m.group(1).lower() + "/" + path[m.end():].replace("\\", "/") if m else path
221
+
222
+
223
+ def _windows_project(ctx: dict) -> bool:
224
+ """A project on Windows: there a backslash separates folders too (..\\..\\x climbs out exactly as ../../x does)."""
225
+ return bool(_WIN_ABS.match(str(ctx.get("directory") or "")))
226
+
227
+
209
228
  def _sensitive_path(abs_path: str) -> bool:
210
229
  parts = abs_path.split("/")
211
230
  base = parts[-1] or ""
@@ -219,7 +238,7 @@ def _sensitive_path(abs_path: str) -> bool:
219
238
 
220
239
 
221
240
  def _project_dir(directory: Any) -> str:
222
- d = _js.js_string(directory) if _js.truthy(directory) else ""
241
+ d = windows_to_posix(_js.js_string(directory)) if _js.truthy(directory) else ""
223
242
  return re.sub(r"/+\Z", "", posix_normalize(d))
224
243
 
225
244
 
@@ -246,6 +265,7 @@ def _word_stays_in_project(w: str, ctx: dict) -> bool:
246
265
  v = v[eq + 1:]
247
266
  if not v:
248
267
  return True
268
+ v = windows_to_posix(v.replace("\\", "/") if _windows_project(ctx) and not v.startswith("~") else v)
249
269
  if "$" in v or re.search(r"(^|/)\.\.(/|\Z)", v):
250
270
  return False
251
271
  if v.startswith("~"):
@@ -333,6 +353,8 @@ def _edit_inside_project(patterns: Any, ctx: dict) -> bool:
333
353
  for p in patterns:
334
354
  if not isinstance(p, str) or not p or re.search(r"[*?\[\]{}]", p):
335
355
  return False # a glob is not a precise target
356
+ if _windows_project(ctx):
357
+ p = windows_to_posix(p.replace("\\", "/") if not _WIN_ABS.match(p) else p)
336
358
  a = posix_normalize(p if p.startswith("/") else d + "/" + p)
337
359
  r = next((x for x in roots if a.startswith(x + "/")), "")
338
360
  if not (r and not _sensitive_path(a[len(r):])):
@@ -33,7 +33,7 @@ import httpx
33
33
 
34
34
  from . import _js
35
35
  from .folders import allowed_folders
36
- from .auto import SCRATCH_DIR, action_for, classify_deterministic, log_record, parse_verdict, reviewer_prompt, scratch_paths
36
+ from .auto import SCRATCH_DIR, windows_to_posix, action_for, classify_deterministic, log_record, parse_verdict, reviewer_prompt, scratch_paths
37
37
  from .policy import SUBAGENT_GATED
38
38
 
39
39
  REVIEW_TITLE = "witbitz-auto-review" # the page never lists a session with this title
@@ -146,7 +146,7 @@ class AutoRunner:
146
146
  doc = _js.parse(self._state_path.read_text(encoding="utf-8"))
147
147
  sessions = doc.get("sessions") if isinstance(doc, dict) else None
148
148
  for sid, v in (sessions.items() if isinstance(sessions, dict) else []):
149
- if sid.startswith("ses") and isinstance(v, dict) and isinstance(v.get("dir"), str) and v["dir"].startswith("/"):
149
+ if sid.startswith("ses") and isinstance(v, dict) and isinstance(v.get("dir"), str) and windows_to_posix(v["dir"]).startswith("/"):
150
150
  at = _js.to_number(v.get("at", _js.UNDEFINED))
151
151
  self._auto[sid] = {"dir": v["dir"], "at": _js.clean_number(at) if _js.truthy(at) else 0}
152
152
  except Exception as e:
@@ -470,7 +470,8 @@ class AutoRunner:
470
470
  if not isinstance(session_id, str) or not _SESSION_ID.fullmatch(session_id):
471
471
  return False
472
472
  if on:
473
- if not isinstance(directory, str) or not directory.startswith("/") or _js.utf16_len(directory) > 1024:
473
+ # Windows: C:\\Users\\… is absolute too (auto.windows_to_posix); the folder is kept as given for OpenCode's API.
474
+ if not isinstance(directory, str) or not windows_to_posix(directory).startswith("/") or _js.utf16_len(directory) > 1024:
474
475
  return False
475
476
  self._auto[session_id] = {"dir": directory, "at": self._now()} # an existing key keeps its place, as Map.set does
476
477
  else:
@@ -63,11 +63,15 @@ def computer_label() -> dict:
63
63
  def open_browser(url: str) -> bool:
64
64
  """WSL has no browser of its own: ask Windows (wslview, then cmd's start, then explorer)."""
65
65
  if ports.on_wsl():
66
+ # WAIT for the hand-off (~0.4 s measured): a second copy started from the Start menu finds the first, opens its page
67
+ # and exits — and an opener still running then was cut off with it (the owner, 2026-10-09: "the wsl does not open
68
+ # anything"). From a Windows folder, or cmd says "UNC paths are not supported".
69
+ cwd = "/mnt/c" if os.path.isdir("/mnt/c") else None
66
70
  for argv in (["wslview", url], ["cmd.exe", "/c", "start", "", url], ["explorer.exe", url]):
67
71
  try:
68
- subprocess.Popen(argv, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # noqa: S603 — fixed argv, our own URL
72
+ subprocess.run(argv, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cwd, timeout=15, check=False) # noqa: S603 — fixed argv, our own URL
69
73
  return True
70
- except OSError:
74
+ except (OSError, subprocess.SubprocessError):
71
75
  continue
72
76
  return False
73
77
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.6.2
3
+ Version: 1.6.4
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -629,3 +629,52 @@ def test_wiring_the_page_switches_auto_and_the_verdict_comes_back(make, tmp_path
629
629
  await connector.stop()
630
630
 
631
631
  run(go())
632
+
633
+
634
+ # ── WINDOWS (the owner, 2026-10-09: "The auto mode is not working (windows)") ────────────────────────────────────────────
635
+ # Every rule read "absolute" as a leading /: Auto would not switch on for C:\…, and C:/… in a command read as INSIDE the
636
+ # project. A Windows request now gets exactly the decision its Linux twin gets.
637
+ _WIN_TWINS = [
638
+ ("ls src", "ls src"),
639
+ ("cat /etc/hosts", "cat C:/Windows/win.ini"),
640
+ ("cat /home/u/repo/src/a.py", "cat C:/Users/u/repo/src/a.py"),
641
+ ("cat /home/u/repo/.git/config", "cat /c/Users/u/repo/.git/config"),
642
+ ]
643
+ _WIN_EDITS = [("src/a.py", "src\\a.py"), ("../../x", "..\\..\\x"), ("/home/u/repo/src/a.py", "C:\\Users\\u\\repo\\src\\a.py"),
644
+ ("/home/u/other/a.py", "C:\\Users\\u\\other\\a.py")]
645
+
646
+
647
+ def _stage(req, directory, home):
648
+ r = classify_deterministic({"id": "per_x", "sessionID": "ses_x", "always": [], **req}, directory=directory, home=home)
649
+ return r["stage"] if r else "review"
650
+
651
+
652
+ @pytest.mark.parametrize("lin,win", _WIN_TWINS)
653
+ def test_a_windows_command_is_judged_like_its_linux_twin(lin, win):
654
+ b = lambda c: {"permission": "bash", "patterns": [c], "metadata": {"command": c}} # noqa: E731
655
+ assert _stage(b(win), "C:\\Users\\u\\repo", "C:\\Users\\u") == _stage(b(lin), "/home/u/repo", "/home/u")
656
+
657
+
658
+ @pytest.mark.parametrize("lin,win", _WIN_EDITS)
659
+ def test_a_windows_edit_is_judged_like_its_linux_twin(lin, win):
660
+ e = lambda p: {"permission": "edit", "patterns": [p], "metadata": {}} # noqa: E731
661
+ assert _stage(e(win), "C:\\Users\\u\\repo", "C:\\Users\\u") == _stage(e(lin), "/home/u/repo", "/home/u")
662
+
663
+
664
+ def test_outside_the_project_on_windows_is_never_allowed_by_itself():
665
+ assert _stage({"permission": "bash", "patterns": ["cat C:/Windows/win.ini"], "metadata": {"command": "cat C:/Windows/win.ini"}},
666
+ "C:\\Users\\u\\repo", "C:\\Users\\u") != "fast-allow"
667
+ assert _stage({"permission": "edit", "patterns": ["..\\..\\x"], "metadata": {}}, "C:\\Users\\u\\repo", "C:\\Users\\u") != "fast-allow"
668
+
669
+
670
+ def test_auto_switches_on_for_a_windows_folder_and_talks_to_opencode_with_it_as_given(tmp_path):
671
+ win = "C:\\Users\\u\\repo"
672
+
673
+ async def body(oc, runner, verdicts, make, client):
674
+ assert runner.set_auto("ses_main", win, True), "C:\\… is a project folder (it was refused: Auto never ran on Windows)"
675
+ assert runner.set_auto("ses_main", "relative\\dir", True) is False
676
+ assert runner.set_auto("ses_main", win, True)
677
+ oc.ask(permission="bash", patterns=["rm -rf ~"], metadata={"command": "rm -rf ~"})
678
+ assert await until(lambda: oc.replies(), 4)
679
+ assert oc.replies()[0]["dir"] == win, "OpenCode is asked with the folder exactly as it named it"
680
+ with_runner(tmp_path, body)
@@ -263,3 +263,16 @@ def test_quit_stops_serving_and_ends_the_manager():
263
263
  await m.shutdown()
264
264
  await srv.close()
265
265
  run(go())
266
+
267
+
268
+ def test_in_wsl_the_page_opener_is_waited_for(monkeypatch):
269
+ """A second copy (the Start-menu entry) opens the first one's page and exits: an opener still running was cut off with
270
+ it (the owner, 2026-10-09: "the wsl does not open anything")."""
271
+ import subprocess as sp
272
+ calls = []
273
+ monkeypatch.setattr(ui.ports, "on_wsl", lambda: True)
274
+ monkeypatch.setattr(ui.subprocess, "run", lambda argv, **kw: calls.append((argv, kw)) or sp.CompletedProcess(argv, 0))
275
+ monkeypatch.setattr(ui.subprocess, "Popen", lambda *a, **k: (_ for _ in ()).throw(AssertionError("never fire-and-forget")))
276
+ assert ui.open_browser("http://127.0.0.1:4291/?t=x") is True
277
+ argv, kw = calls[0]
278
+ assert argv == ["wslview", "http://127.0.0.1:4291/?t=x"] and kw["timeout"] == 15
File without changes
File without changes
File without changes