witbitz-code 1.6.0__tar.gz → 1.6.1__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 (98) hide show
  1. {witbitz_code-1.6.0/src/witbitz_code.egg-info → witbitz_code-1.6.1}/PKG-INFO +1 -1
  2. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/pyproject.toml +1 -1
  3. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/__init__.py +1 -1
  4. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/uninstall.py +72 -5
  5. {witbitz_code-1.6.0 → witbitz_code-1.6.1/src/witbitz_code.egg-info}/PKG-INFO +1 -1
  6. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_uninstall.py +37 -1
  7. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/LICENSE +0 -0
  8. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/README.md +0 -0
  9. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/setup.cfg +0 -0
  10. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/__main__.py +0 -0
  11. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/_js.py +0 -0
  12. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/account.py +0 -0
  13. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/asks.py +0 -0
  14. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/attachments.py +0 -0
  15. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/auto.py +0 -0
  16. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/auto_runner.py +0 -0
  17. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/cli.py +0 -0
  18. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/codekeys.py +0 -0
  19. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/codetools.py +0 -0
  20. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/compress.py +0 -0
  21. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/computers.py +0 -0
  22. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/confidential.py +0 -0
  23. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/confidential_models.json +0 -0
  24. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/connector.py +0 -0
  25. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/devicelink.py +0 -0
  26. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/dirs.py +0 -0
  27. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/folders.py +0 -0
  28. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/gateway.py +0 -0
  29. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/keys.py +0 -0
  30. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/link.py +0 -0
  31. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/mkdir.py +0 -0
  32. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/models.py +0 -0
  33. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/net.py +0 -0
  34. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/offered_models.json +0 -0
  35. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  36. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  37. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  38. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  39. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/outputs.py +0 -0
  40. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/pair.py +0 -0
  41. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/pairings.py +0 -0
  42. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/payers.py +0 -0
  43. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/plugins.py +0 -0
  44. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/policy.py +0 -0
  45. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/ports.py +0 -0
  46. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/qr.py +0 -0
  47. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/receipt.py +0 -0
  48. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/relay.py +0 -0
  49. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/seen.py +0 -0
  50. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/tinfoil_read.py +0 -0
  51. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/tools_catalog.json +0 -0
  52. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/tools_probe.py +0 -0
  53. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/toolsetup.py +0 -0
  54. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/ui.py +0 -0
  55. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/ui_static/app.css +0 -0
  56. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/ui_static/app.js +0 -0
  57. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/ui_static/index.html +0 -0
  58. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/uitools.py +0 -0
  59. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/winagent.py +0 -0
  60. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/winbg.py +0 -0
  61. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/winconsole.py +0 -0
  62. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/wizard.py +0 -0
  63. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code/wslstart.py +0 -0
  64. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
  65. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  66. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  67. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code.egg-info/requires.txt +0 -0
  68. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/src/witbitz_code.egg-info/top_level.txt +0 -0
  69. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_asks.py +0 -0
  70. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_attachments.py +0 -0
  71. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_auto.py +0 -0
  72. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_cli.py +0 -0
  73. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_codekeys.py +0 -0
  74. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_codetools.py +0 -0
  75. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_confidential.py +0 -0
  76. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_connector_e2e.py +0 -0
  77. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_devicelink.py +0 -0
  78. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_dirs.py +0 -0
  79. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_files_and_compress.py +0 -0
  80. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_folders.py +0 -0
  81. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_outputs.py +0 -0
  82. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_pair_flows.py +0 -0
  83. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_plugins.py +0 -0
  84. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_policy.py +0 -0
  85. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_ports.py +0 -0
  86. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_ports_win.py +0 -0
  87. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_registry.py +0 -0
  88. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_relay_codec.py +0 -0
  89. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_setup.py +0 -0
  90. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_tools_probe.py +0 -0
  91. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_toolsetup.py +0 -0
  92. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_ui.py +0 -0
  93. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_uitools.py +0 -0
  94. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_winagent.py +0 -0
  95. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_winbg.py +0 -0
  96. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_winconsole.py +0 -0
  97. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/tests/test_windows_files.py +0 -0
  98. {witbitz_code-1.6.0 → witbitz_code-1.6.1}/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.0
3
+ Version: 1.6.1
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.0"
7
+ version = "1.6.1"
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.0"
7
+ __version__ = "1.6.1"
@@ -23,7 +23,7 @@ from dataclasses import dataclass, field
23
23
  from pathlib import Path
24
24
  from typing import Any
25
25
 
26
- from . import keys, plugins
26
+ from . import keys, plugins, ports
27
27
  from . import pairings as pp
28
28
  from .wizard import find_opencode
29
29
 
@@ -138,6 +138,8 @@ def opencode_holders(data_dir: str) -> list[dict]:
138
138
  cmd = ""
139
139
  hits.append({"pid": int(p), "cmd": cmd})
140
140
  return hits
141
+ if sys.platform == "win32": # no /proc, no lsof: every opencode.exe of this user's (the owner, 2026-10-09: the background
142
+ return win_opencode_processes() # copy's OpenCode held opencode.db and the delete died "WinError 32")
141
143
  files = [os.path.join(data_dir, f) for f in ("opencode.db", "opencode.db-wal", "opencode.db-shm") if os.path.exists(os.path.join(data_dir, f))]
142
144
  if not files or not shutil.which("lsof"):
143
145
  return hits
@@ -150,6 +152,55 @@ def opencode_holders(data_dir: str) -> list[dict]:
150
152
  return hits
151
153
 
152
154
 
155
+ def win_opencode_processes(*, run: Callable[[list[str]], str] | None = None, user: str | None = None) -> list[dict]:
156
+ """Windows: this user's opencode.exe processes (tasklist, one CSV row each)."""
157
+ run = run or ports._run_text
158
+ user = os.environ.get("USERNAME", "") if user is None else user
159
+ hits = []
160
+ for line in run(["tasklist", "/V", "/FO", "CSV", "/NH", "/FI", "IMAGENAME eq opencode.exe"]).splitlines():
161
+ row = ports.parse_tasklist_row(line)
162
+ if row and (not user or row["user"].rsplit("\\", 1)[-1].lower() == user.lower()):
163
+ hits.append({"pid": row["pid"], "cmd": row["image"]})
164
+ return hits
165
+
166
+
167
+ def running_witbitz(*, lock: Path | None = None, pairings: Callable[[], list] | None = None,
168
+ owners: Callable[[int], list] | None = None, alive: Callable[[int], bool] | None = None) -> list[dict]:
169
+ """What runs witbitz-code here now: the page's manager (its ui.json names it — in the background it has no window to
170
+ close) and whatever holds each paired OpenCode port or its model proxy's (port + 100): a serve, its OpenCode."""
171
+ lock = lock or pp.pairings_path().parent / "ui.json"
172
+ pairings = pairings or (lambda: pp.load_pairings(log=lambda _m: None))
173
+ owners = owners or ports.port_owners
174
+ alive = alive or _alive
175
+ found: list[dict] = []
176
+ try:
177
+ pid = json.loads(lock.read_text(encoding="utf-8")).get("pid")
178
+ if isinstance(pid, int) and pid != os.getpid() and alive(pid):
179
+ found.append({"pid": pid, "cmd": "the Witbitz Code page"})
180
+ except (OSError, ValueError, AttributeError):
181
+ pass
182
+ for p in pairings():
183
+ tail = str(p.get("opencodeUrl") or "").rsplit(":", 1)[-1].rstrip("/")
184
+ port = int(tail) if tail.isdigit() else 0
185
+ for q in ([port, port + 100] if port else []):
186
+ for o in owners(q):
187
+ if o["pid"] != os.getpid() and all(o["pid"] != x["pid"] for x in found):
188
+ found.append({"pid": int(o["pid"]), "cmd": str(o.get("cmd") or "")[:70]})
189
+ return found
190
+
191
+
192
+ def _alive(pid: int) -> bool:
193
+ if sys.platform == "win32":
194
+ return _win_alive(pid)
195
+ try:
196
+ os.kill(pid, 0)
197
+ return True
198
+ except ProcessLookupError:
199
+ return False
200
+ except PermissionError:
201
+ return False # another user's: not ours
202
+
203
+
153
204
  def _win_alive(pid: int) -> bool:
154
205
  """Windows: is `pid` still running? (OpenProcess + GetExitCodeProcess; access denied = another user's, still running.)"""
155
206
  import ctypes # noqa: PLC0415 — Windows only
@@ -287,6 +338,7 @@ class Deps:
287
338
  sessions: Callable[[], dict] = None # type: ignore[assignment]
288
339
  delete_sessions: Callable[[], None] = None # type: ignore[assignment]
289
340
  opencode_processes: Callable[[], list] = lambda: opencode_holders(str(keys.auth_path().parent))
341
+ running: Callable[[], list] = running_witbitz
290
342
  stop_process: Callable[[int], bool] = stop_process
291
343
  opencode: Callable[[], dict] = opencode_here
292
344
  remove_opencode_program: Callable[[dict], dict] = remove_opencode_program
@@ -332,6 +384,12 @@ async def run_uninstall(d: Deps) -> dict:
332
384
  how = {"installer": "from OpenCode's installer, in ~/.opencode", "npm": "with npm", "brew": "with Homebrew"}.get(oc["kind"])
333
385
  remove_oc = ask(d.remove_opencode, f"Also remove OpenCode itself (installed {how})? [y/N] ") if oc["kind"] not in ("none", "other") else False
334
386
 
387
+ # The running witbitz-code first — the page's manager in the background, a serve, their OpenCode: it holds the files
388
+ # about to go (Windows refuses to delete an open file) and would keep answering the phone. Continue already said yes.
389
+ for p in d.running():
390
+ d.say(f"✓ Stopped the running witbitz-code: {p['cmd'] or 'a program'} (process {p['pid']})" if d.stop_process(p["pid"])
391
+ else f"✖ Could not stop {p['cmd'] or 'a program'} (process {p['pid']}) — close it, then run uninstall again")
392
+
335
393
  # Sessions: an OpenCode that has its database open would write it back — never delete under it.
336
394
  while remove_sessions:
337
395
  procs = d.opencode_processes()
@@ -376,8 +434,11 @@ async def run_uninstall(d: Deps) -> dict:
376
434
  d.remove_path(notes["root"])
377
435
  d.say(f"✓ Deleted {notes['root']}")
378
436
  if remove_sessions:
379
- d.delete_sessions()
380
- d.say(f"✓ Deleted the OpenCode sessions in {sessions['dir']} (provider logins kept)")
437
+ stuck = d.delete_sessions() or []
438
+ if stuck:
439
+ d.say(f"✖ Could not delete {', '.join(stuck[:4])} in {sessions['dir']} — another program still has it open; close OpenCode and delete it yourself")
440
+ else:
441
+ d.say(f"✓ Deleted the OpenCode sessions in {sessions['dir']} (provider logins kept)")
381
442
  oc_gone = False
382
443
  if remove_oc:
383
444
  try:
@@ -454,12 +515,18 @@ def sessions_info() -> dict:
454
515
  return {"dir": str(d), "exists": exists}
455
516
 
456
517
 
457
- def delete_sessions() -> None:
518
+ def delete_sessions() -> list[str]:
519
+ """→ the names that could not be deleted (Windows: a file another program still has open), never a crash."""
458
520
  d = keys.auth_path().parent
521
+ left = []
459
522
  for f in d.iterdir():
460
523
  if f.name == "auth.json":
461
524
  continue
462
- shutil.rmtree(f, ignore_errors=True) if f.is_dir() and not f.is_symlink() else f.unlink(missing_ok=True)
525
+ try:
526
+ shutil.rmtree(f, ignore_errors=True) if f.is_dir() and not f.is_symlink() else f.unlink(missing_ok=True)
527
+ except OSError:
528
+ left.append(f.name)
529
+ return left
463
530
 
464
531
 
465
532
  def saved_keys() -> list[str]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.6.0
3
+ Version: 1.6.1
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
@@ -72,7 +72,7 @@ class Fake:
72
72
  remove_opencode_program=lambda info: self.done.append(("opencode",)) or {"ok": True, "said": []},
73
73
  package=lambda: {"kind": self._pkg, "hint": "python3 -m pip uninstall witbitz-code"},
74
74
  remove_package=lambda info: self.done.append(("package",)) or {"ok": True},
75
- **{"login_start": lambda: None, "menu_remove": lambda: False, **over}) # never this machine's Windows Startup folder or Start menu
75
+ **{"login_start": lambda: None, "menu_remove": lambda: False, "running": lambda: [], **over}) # never this machine's Windows Startup folder or Start menu
76
76
 
77
77
 
78
78
  PAIR = {"name": "desk", "account": "a@x.com", "idx": {"room": "r"}, "computerId": "c"}
@@ -174,3 +174,39 @@ def test_on_wsl_the_windows_startup_shortcut_setup_made_goes_too():
174
174
  assert gone == ["Witbitz Code (WSL UbuntuD).lnk", "menu"]
175
175
  assert any('Removed "Witbitz Code (WSL UbuntuD)" from the Start menu' in line for line in f.said)
176
176
  assert any('Removed the Windows Startup shortcut "Witbitz Code (WSL UbuntuD)"' in line for line in f.said)
177
+
178
+
179
+ def test_the_running_witbitz_code_is_stopped_before_anything_is_deleted():
180
+ """The owner, 2026-10-09 (Windows): the background copy's OpenCode held opencode.db and the delete died WinError 32."""
181
+ f = Fake(pairs=[PAIR], sessions=True, answers=["y", "y"])
182
+ run(un.run_uninstall(f.deps(running=lambda: [{"pid": 8420, "cmd": "the Witbitz Code page"}, {"pid": 19228, "cmd": "opencode.exe"}])))
183
+ stops = [i for i, x in enumerate(f.done) if x[0] == "stop"]
184
+ assert [f.done[i] for i in stops] == [("stop", 8420), ("stop", 19228)]
185
+ assert stops[-1] < f.done.index(("sessions",)) and stops[-1] < f.done.index(("rm", "/h/.witbitz/code"))
186
+ assert any("Stopped the running witbitz-code: the Witbitz Code page (process 8420)" in line for line in f.said)
187
+
188
+
189
+ def test_a_session_file_still_open_is_said_not_a_crash():
190
+ f = Fake(sessions=True, answers=["y", "y"])
191
+ d = f.deps()
192
+ d.delete_sessions = lambda: ["opencode.db"]
193
+ r = run(un.run_uninstall(d))
194
+ assert r.get("done") is not False
195
+ assert any("Could not delete opencode.db" in line for line in f.said)
196
+
197
+
198
+ def test_what_runs_witbitz_code_is_the_page_s_manager_and_the_paired_ports_owners(tmp_path):
199
+ lock = tmp_path / "ui.json"
200
+ lock.write_text('{"port": 4290, "token": "t", "pid": 8420}')
201
+ owners = {4097: [{"pid": 19228, "cmd": "opencode.exe"}], 4197: [{"pid": 8420, "cmd": "python.exe"}]}
202
+ got = un.running_witbitz(lock=lock, pairings=lambda: [{"opencodeUrl": "http://127.0.0.1:4097"}],
203
+ owners=lambda q: owners.get(q, []), alive=lambda pid: pid == 8420)
204
+ assert got == [{"pid": 8420, "cmd": "the Witbitz Code page"}, {"pid": 19228, "cmd": "opencode.exe"}]
205
+ lock.write_text('{"pid": 8420}')
206
+ assert un.running_witbitz(lock=lock, pairings=lambda: [], owners=lambda q: [], alive=lambda pid: False) == []
207
+
208
+
209
+ def test_windows_finds_opencode_by_name():
210
+ out = '"opencode.exe","19228","Console","1","95,000 K","Running","ALONASUS\\shter","0:00:03","N/A"\n' \
211
+ '"opencode.exe","555","Console","1","1 K","Running","OTHER\\bob","0:00:01","N/A"\n'
212
+ assert un.win_opencode_processes(run=lambda argv: out, user="shter") == [{"pid": 19228, "cmd": "opencode.exe"}]
File without changes
File without changes
File without changes