Add native Windows host daemon (bleak/WinRT)

Adds a Windows port of the host daemon so the Clawdmeter stays connected
on Windows independent of WSL. Mirrors the macOS daemon and speaks the
existing GATT data service unchanged — no firmware changes.

- claude_usage_daemon_windows.py: Windows-local OAuth token read +
  Anthropic poll + BLE scan/connect/write, with auto-reconnect
  (connect-retry wrapper, zombie-link break, split fast/slow backoff)
- tray_windows.py: pystray login-startup tray app (status icon + Quit)
- autostart_windows.py: winreg HKCU\Run autostart via pythonw.exe
- icon_assets.py: per-state tray icons composited from logo.h
- install-windows.ps1 + daemon/README-windows.md: turnkey setup
- pytest suite: token / poll / reconnect / tray / autostart / no-WSL guard
This commit is contained in:
kvenanzi
2026-06-02 16:39:07 -05:00
parent 7df64eecec
commit eb70456755
20 changed files with 3592 additions and 2 deletions
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""Unit tests for daemon/autostart_windows.py — APP-01.
Covers the winreg HKCU\\Run enable/disable/is_enabled login-autostart toggle.
winreg is NOT importable off-Windows; these tests patch it via
patch("daemon.autostart_windows.winreg", ...) so they run on any platform.
Run: python -m pytest daemon/tests/test_windows_autostart.py -x -q
"""
from unittest.mock import MagicMock, patch, call
import pytest
# ---------------------------------------------------------------------------
# Helpers — build a fake winreg module with the attributes autostart_windows
# references. Using a MagicMock as the module means all attribute accesses
# on it (HKEY_CURRENT_USER, KEY_SET_VALUE, etc.) automatically produce child
# MagicMocks, which is exactly what we want.
# ---------------------------------------------------------------------------
def _make_winreg_mock(*, query_raises=False):
"""Return a configured MagicMock that stands in for the winreg module."""
winreg = MagicMock()
# Constants — assign simple sentinel values so equality checks work.
winreg.HKEY_CURRENT_USER = "HKEY_CURRENT_USER"
winreg.KEY_SET_VALUE = 0x0002
winreg.KEY_QUERY_VALUE = 0x0001
winreg.REG_SZ = 1
# OpenKey is used as a context manager; return a MagicMock key handle that
# supports __enter__ / __exit__.
key_handle = MagicMock()
key_handle.__enter__ = MagicMock(return_value=key_handle)
key_handle.__exit__ = MagicMock(return_value=False)
winreg.OpenKey = MagicMock(return_value=key_handle)
# QueryValueEx behaviour is configured by the caller.
if query_raises:
winreg.QueryValueEx = MagicMock(side_effect=FileNotFoundError("not found"))
else:
winreg.QueryValueEx = MagicMock(return_value=("some_command", 1))
return winreg, key_handle
# ---------------------------------------------------------------------------
# test_enable_writes_run_value
# ---------------------------------------------------------------------------
def test_enable_writes_run_value():
"""enable() opens HKCU Run key with KEY_SET_VALUE and calls SetValueEx with
value name 'Clawdmeter' and type REG_SZ."""
winreg, key_handle = _make_winreg_mock()
with patch("daemon.autostart_windows.winreg", winreg):
import daemon.autostart_windows as mod
mod.enable()
# OpenKey must have been called with HKCU and the Run key path
winreg.OpenKey.assert_called_once_with(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Run",
0,
winreg.KEY_SET_VALUE,
)
# SetValueEx must have been called with the correct value name and type
winreg.SetValueEx.assert_called_once()
args = winreg.SetValueEx.call_args[0]
assert args[0] is key_handle, "SetValueEx first arg must be the opened key handle"
assert args[1] == "Clawdmeter", "Value name must be 'Clawdmeter'"
assert args[3] == winreg.REG_SZ, "Value type must be REG_SZ"
# ---------------------------------------------------------------------------
# test_command_uses_pythonw
# ---------------------------------------------------------------------------
def test_command_uses_pythonw():
"""The command string written by enable() contains 'pythonw.exe', does NOT
contain a bare 'python.exe' token (D-08, no console), and is quoted (starts
with a double-quote character)."""
winreg, key_handle = _make_winreg_mock()
captured_commands = []
def capture_set_value_ex(key, name, reserved, reg_type, value):
captured_commands.append(value)
winreg.SetValueEx = MagicMock(side_effect=capture_set_value_ex)
with patch("daemon.autostart_windows.winreg", winreg):
import daemon.autostart_windows as mod
mod.enable()
assert len(captured_commands) == 1, "SetValueEx must have been called exactly once"
cmd = captured_commands[0]
# Must reference pythonw.exe (D-08)
assert "pythonw.exe" in cmd, f"Command must contain 'pythonw.exe'; got: {cmd!r}"
# Must NOT contain a bare 'python.exe' (without the 'w') as a standalone token
# A command like '"...pythonw.exe" ...' is fine; '"...python.exe" ...' is not.
import re
assert not re.search(r'(?<![a-z])python\.exe', cmd), (
f"Command must not reference a bare 'python.exe'; got: {cmd!r}"
)
# Must start with a double-quote (paths are quoted for space safety)
assert cmd.startswith('"'), (
f"Command must start with '\"' (quoted path); got: {cmd!r}"
)
# ---------------------------------------------------------------------------
# test_disable_idempotent
# ---------------------------------------------------------------------------
def test_disable_idempotent():
"""disable() calls DeleteValue; when DeleteValue raises FileNotFoundError,
disable() swallows it and returns without raising (idempotent-on-missing)."""
winreg, key_handle = _make_winreg_mock()
winreg.DeleteValue = MagicMock(side_effect=FileNotFoundError("not found"))
with patch("daemon.autostart_windows.winreg", winreg):
import daemon.autostart_windows as mod
# Must not raise even though DeleteValue raises FileNotFoundError
mod.disable() # no exception expected
winreg.DeleteValue.assert_called_once()
def test_disable_calls_delete_value_with_correct_name():
"""disable() calls DeleteValue with the value name 'Clawdmeter'."""
winreg, key_handle = _make_winreg_mock()
winreg.DeleteValue = MagicMock()
with patch("daemon.autostart_windows.winreg", winreg):
import daemon.autostart_windows as mod
mod.disable()
winreg.DeleteValue.assert_called_once()
args = winreg.DeleteValue.call_args[0]
assert args[1] == "Clawdmeter", f"DeleteValue must target 'Clawdmeter'; got {args[1]!r}"
# ---------------------------------------------------------------------------
# test_is_enabled
# ---------------------------------------------------------------------------
def test_is_enabled_true_when_value_present():
"""is_enabled() returns True when QueryValueEx succeeds (value is present)."""
winreg, key_handle = _make_winreg_mock(query_raises=False)
with patch("daemon.autostart_windows.winreg", winreg):
import daemon.autostart_windows as mod
result = mod.is_enabled()
assert result is True, "is_enabled() must return True when QueryValueEx succeeds"
def test_is_enabled_false_when_value_absent():
"""is_enabled() returns False when QueryValueEx raises FileNotFoundError."""
winreg, key_handle = _make_winreg_mock(query_raises=True)
with patch("daemon.autostart_windows.winreg", winreg):
import daemon.autostart_windows as mod
result = mod.is_enabled()
assert result is False, "is_enabled() must return False when QueryValueEx raises FileNotFoundError"