Skip to content

Commit 94c3299

Browse files
committed
Merge upstream/master into issue-966-per-tool-approval
Brings in the JSDOM behaviour harness (homeassistant-ai#1425) and dependent updates.
2 parents d27f583 + 31eb00e commit 94c3299

20 files changed

Lines changed: 4471 additions & 417 deletions

.github/workflows/pr.yml

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,53 @@ jobs:
128128
timeout-minutes: 5
129129

130130
steps:
131-
- name: Install git for submodule checkout
132-
run: apt-get update -qq && apt-get install -y -qq git >/dev/null 2>&1
131+
- name: Restore apt download cache
132+
# Cache /var/cache/apt/archives so the nodejs .deb (~30 MB) and its
133+
# deps don't re-download on every PR — the bulk of "Install git and
134+
# Node.js" was the network download, not the extract. apt-get
135+
# install still runs (unpack is fast); we're just skipping the
136+
# download leg on cache hit.
137+
uses: actions/cache@v5
138+
with:
139+
path: /var/cache/apt/archives
140+
key: apt-trixie-git-nodejs-npm-v1
141+
142+
- name: Install git and Node.js
143+
# Node + npm power the JS behaviour tests under tests/src/unit/
144+
# (harness in tests/js/) — they spawn `node tests/js/harness.mjs`
145+
# to drive rendered <script> bodies through JSDOM. Without node
146+
# the tests skip; install here so coverage actually runs.
147+
run: |
148+
# Keep cached .debs after install so the cache stays warm for
149+
# future runs (default Debian behaviour deletes them).
150+
rm -f /etc/apt/apt.conf.d/docker-clean
151+
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \
152+
> /etc/apt/apt.conf.d/keep-downloaded
153+
apt-get update -qq
154+
apt-get install -y -qq git nodejs npm >/dev/null 2>&1
133155
134156
- uses: actions/checkout@v6
135157
with:
136158
submodules: true
137159

138-
- name: Install dependencies
160+
- name: Install Python dependencies
139161
run: uv sync --all-extras --dev
140162

163+
- name: Restore JS test node_modules cache
164+
# Lockfile-keyed so a dependency bump invalidates cleanly. `npm ci`
165+
# is still safe to run on cache hit (it short-circuits when the
166+
# tree already matches the lockfile).
167+
uses: actions/cache@v5
168+
with:
169+
path: tests/js/node_modules
170+
key: tests-js-node-modules-${{ hashFiles('tests/js/package-lock.json') }}
171+
172+
- name: Install JS test dependencies (jsdom, esbuild)
173+
# `npm ci` enforces package-lock.json — same reproducibility
174+
# discipline as uv.lock on the Python side. No-op on cache hit
175+
# if node_modules matches the lockfile.
176+
run: npm ci --prefix tests/js
177+
141178
- name: Run unit tests
142179
run: uv run pytest tests/src/unit/ -n auto --tb=short -v
143180

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,6 @@ tests/initial_test_state/custom_components/hacs/hacs_frontend/
9494
.auto-claude/
9595
.worktrees/
9696
.claude_settings.json
97+
98+
# Node modules — site (Astro) and tests/js (JSDOM behaviour harness)
99+
node_modules/

AGENTS.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,70 @@ src/ha_mcp/
543543

544544
**Tool Completion Semantics**: Tools should wait for operations to complete before returning, with optional `wait` parameter for control.
545545

546+
## JS Behaviour Testing (`tests/js/`, `tests/src/unit/_js_harness.py`)
547+
548+
Every rendered `<script>` body in the repo (`src/ha_mcp/settings_ui.py`,
549+
`src/ha_mcp/auth/consent_form.py`, every `.astro` page under `site/src/`)
550+
gets parse coverage automatically via
551+
`tests/src/unit/test_rendered_scripts_parse.py`. The discovery walker in
552+
`_js_harness.py::discover_script_surfaces` picks up new surfaces on its
553+
next run — no registration needed when you add a new UI.
554+
555+
For behavioural tests (`restartInProgress` guard, wizard state machine,
556+
copy-button idempotency, etc.), use the JSDOM harness:
557+
558+
```python
559+
from ._js_harness import extract_script_body, run_script
560+
561+
script = extract_script_body(rendered_html)
562+
result = run_script(
563+
script,
564+
initial_html="<!DOCTYPE html>...",
565+
fetch_map={"/api/foo": {"status": 200, "json": {...}}},
566+
broadcast_events=[{"channel": "ch-name", "data": {"type": "..."}}],
567+
invoke="await window.someExposedFn();",
568+
)
569+
assert result.reloads == 1
570+
assert result.broadcasts_of_type("restart-required")
571+
```
572+
573+
The harness fakes `setTimeout` / `setInterval` / `Date.now` on a
574+
virtual clock (a 60 s production probe completes in milliseconds of
575+
wall time), stubs `fetch` from a URL pattern map (with optional
576+
`responses: [...]` sequencing for state-flip flows), captures
577+
`location.reload` via JSDOM's `jsdomError` channel (unforgeable IDL
578+
property), and provides a `BroadcastChannel` shim that can be primed
579+
with cross-tab events. `new Date()` / `performance.now()` continue to
580+
report wall time — only the three sources above are faked.
581+
582+
Astro `<script>` blocks without `define:vars` / `is:inline` are
583+
TypeScript by default — pass `language="ts"` to `run_script` and the
584+
harness strips types via esbuild before evaluation. For Astro pages
585+
that need wizard data (`clientsData`, etc. via `define:vars`), use
586+
`extract_astro_frontmatter_vars` + `astro_vars_prelude` to inject the
587+
real production data:
588+
589+
```python
590+
vars_ = extract_astro_frontmatter_vars(astro_path, ["clientsData", ...])
591+
prelude = astro_vars_prelude(vars_)
592+
result = run_script(script, prelude=prelude, ...)
593+
```
594+
595+
CI installs Node + jsdom in the `unit-tests` job (`.github/workflows/pr.yml`).
596+
Local devs without `tests/js/node_modules/` get clean skips.
597+
598+
When adding a new UI surface:
599+
- Python-rendered HTML: register the renderer in
600+
`_js_harness.py::_PY_RENDERERS` so the auto-discovery walker picks
601+
it up for parse coverage.
602+
- Astro page: drop the `.astro` file under `site/src/`; discovery walks
603+
the tree automatically.
604+
- Behavioural tests: add a `test_<surface>_js_behavior.py` module
605+
alongside the existing ones (`test_settings_ui_js_behavior.py`,
606+
`test_astro_setup_js_behavior.py`, `test_astro_tools_js_behavior.py`,
607+
`test_astro_layout_js_behavior.py`, `test_consent_form_js_behavior.py`)
608+
— pattern is one module per UI surface.
609+
546610
## Setup Wizard (`site/src/pages/setup.astro`)
547611

548612
Single-file Astro page that drives the on-site setup flow. Both the metadata (which clients/platforms/connections/deployments exist) and the per-client instruction prose live in this one file.

homeassistant-addon-dev/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: "Home Assistant MCP Server (Dev)"
22
description: "Development channel - AI assistant integration via MCP (unstable)"
3-
version: "7.5.0.dev349"
3+
version: "7.5.0.dev351"
44
slug: "ha_mcp_dev"
55
url: "https://github.com/homeassistant-ai/ha-mcp"
66
stage: experimental

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ ignore = [
145145
"src/ha_mcp/tools/tools_entities.py" = ["C901"]
146146
"src/ha_mcp/tools/tools_registry.py" = ["C901"]
147147
"src/ha_mcp/tools/tools_search.py" = ["C901"]
148-
"src/ha_mcp/tools/tools_utility.py" = ["C901"]
149148
"src/ha_mcp/tools/smart_search.py" = ["C901"]
150149

151150
[tool.pytest.ini_options]
@@ -187,6 +186,7 @@ dev = [
187186
"pytest>=8.4.2",
188187
"pytest-asyncio>=1.1.0",
189188
"pytest-cov>=5.0.0",
189+
"pytest-rerunfailures>=15.0",
190190
"pytest-xdist>=3.8.0",
191191
"requests>=2.25.0",
192192
"lefthook>=1.10.0",

0 commit comments

Comments
 (0)