Commit Graph
15 Commits
Author SHA1 Message Date
Adam SmithandClaude Opus 5 c264f6cb94 test: pin the snapshot fixture to Home Assistant's extension
The ported suite passes in CI and fails locally on the same commit, same
Python (3.14.7) and byte-identical dependency versions (syrupy 6.0.0,
pytest 9.0.3, pytest-homeassistant-custom-component 0.13.365, HA
2026.9.2): tests/test_conversation.py::test_model_args fails with
"Snapshot 'test_model_args[subentry_options0]' does not exist!" even
though tests/snapshots/test_conversation.ambr contains exactly that key.

Cause: syrupy and pytest-homeassistant-custom-component both register a
`snapshot` fixture via the pytest11 entry point, and the plugin loaded
last wins. syrupy's stores snapshots in `__snapshots__/`;
HomeAssistantSnapshotExtension overrides dirname() to `snapshots/`,
which is where core's ported .ambr files are. Entry-point iteration
order is install/filesystem dependent, so which one wins varies per
machine -- confirmed by running with --snapshot-update locally, which
wrote a new file to tests/__snapshots__/ instead of finding the
committed one under tests/snapshots/.

That means the green CI run was luck, not design: any change to install
order could have flipped it, and the failure mode is a *missing*
snapshot, which reads like a stale fixture rather than a harness bug.

Fixed the way core's own tests/conftest.py does it -- redefine
`snapshot` in conftest, since a conftest fixture always takes precedence
over a plugin's. This is the one piece of core's conftest the port
dropped.

Local: 137 passed (was 136 passed, 1 failed). Matches CI's 137.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki
2026-09-17 21:54:27 -04:00
Adam SmithandClaude Opus 5 30880c0958 fix: restore render_readme; the hacs check fails because the repo is private
Corrects the previous commit, which removed `render_readme` from
hacs.json on a wrong diagnosis. Two errors there:

1. `render_readme` IS in HACS_MANIFEST_JSON_SCHEMA
   (custom_components/hacs/utils/validate.py), so it was never the
   schema violation I claimed, and HacsManifest defaults it to False --
   dropping it would have silently stopped HACS rendering the README on
   the integration's HACS page. Restored.

2. The real cause of both remaining failures is that this repository is
   **private**. hacsjson's error text ("has an invalid 'hacs.json'
   file") is raised on `rawhacsjson is None`, not on a schema error --
   a schema error would surface as a humanized voluptuous message.
   get_hacs_json_raw() fetches
   raw.githubusercontent.com/<repo>/<ref>/hacs.json *unauthenticated*
   (wrapped in @return_none_on_exception), and that 404s for a private
   repo. get_integration_manifest() fetches manifest.json the same way,
   which is why integration_manifest reports "expected a dictionary.
   Got None" -- voluptuous validating None, not a bad manifest. Both
   files are in fact valid and manifest.json already carries all six
   keys INTEGRATION_MANIFEST_JSON_SCHEMA requires.

So these last 2/9 checks cannot pass while the repo is private, and no
repo-side change can fix them; they should clear on their own when it
goes public. The other four failures the previous commit addressed
(topics, description, information, brands) did clear -- confirmed in
run 35296482777, which went 6/9 failed -> 2/9 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki
2026-09-17 21:46:16 -04:00
Adam SmithandClaude Opus 5 21702031ff docs: add README and brand icon; drop invalid hacs.json key
The `hacs` validation job has failed on every push since the scaffold
commit (6/9 checks). This fixes the four failures that live in the repo;
the two that live in GitHub metadata (description, topics) are set via
the API and need no commit.

- hacs.json: remove `render_readme`. It is not in the HACS manifest
  schema (valid keys: name, content_in_root, zip_release, filename,
  hide_default_branch, country, homeassistant, hacs,
  persistent_directory), and HACS rejects the whole file on an unknown
  key -- which is what `<Validation hacsjson> failed` was. Expecting
  the cascading `integration_manifest` failure ("expected a dictionary.
  Got None") to clear with it, since manifest.json itself already
  carries every key HACS requires.

- README.md: the repo had none, so the `information` check failed and
  HACS had nothing to render. Documents the one thing this fork adds
  (configurable base URL) and, importantly, the compatibility ceiling:
  upstream calls the Responses API, so an endpoint that only serves
  /v1/chat/completions cannot back the conversation or AI Task
  subentries. Also records which endpoint each subentry needs, that
  upstream's recommended model names and unsupported-model lists are
  OpenAI-specific, the base-URL rules (http/https, no embedded
  credentials, include /v1), and why a blank API key sends the
  `not-required` placeholder.

- brand/icon.png + icon@2x.png: `brands` fails unless the repo ships
  brand assets or the domain is in home-assistant/brands. Shipping
  assets is the option that does not depend on a third-party PR.
  Generated with stdlib zlib/struct (no PIL or ImageMagick in this
  environment) from supersampled SDFs; the generator is scratch, not
  committed. Deliberately neither OpenAI's nor Home Assistant's mark,
  and a placeholder -- replace it with real artwork before release.

Not done here: this fork has no git tag or GitHub release yet. HACS
treats releases as preferred-but-not-required and the action does not
check for one, so it is not part of this failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxJYWgGJtCw13qUHZtG8ki
2026-09-17 21:43:23 -04:00
Adam SmithandClaude Sonnet 5 b96b46495a fix: reauth must preserve base_url; reject credentials embedded in base_url
Critical (review finding 1): async_step_reauth_confirm showed
STEP_USER_DATA_SCHEMA directly instead of prefilling it via
add_suggested_values_to_schema (unlike async_step_user, which does),
so the reauth form displayed DEFAULT_BASE_URL rather than the entry's
configured endpoint. A user rotating their API key would have their
integration silently repointed from e.g. LiteLLM back to
api.openai.com on save. Fixed by prefilling from
self._get_reauth_entry().data, same as the user step. strings.json's
reauth_confirm block now documents the base_url field too.
Regression test: test_reauth_preserves_base_url (asserts both the
shown form's suggested_value and the final stored base_url).

Important (review finding 2): base_url is user-suppliable and could
carry embedded user:pass@ credentials that later surface through
vendored exception logging (e.g. entity.py's "Error talking to
OpenAI: %s") this fork does not and should not edit. Rather than
touch vendored logging, reject credentials at the boundary:
normalize_base_url now raises the new CredentialsInBaseURL (a
subclass of InvalidBaseURL) when the URL has a username or password,
mapped to a distinct "credentials_in_url" form error telling the user
to use the API key field instead. Tests added in test_url_util.py and
test_config_flow.py.

Important (review finding 3): documented on sanitize_url_for_logging
that it is not a general-purpose credential scrubber -- it only masks
what urlparse recognizes as a netloc's user:pass@ authority component,
so a scheme-less/relative string with embedded credentials passes
through unmasked. Safe today because every caller normalizes first
(which now also rejects embedded credentials outright), but it is
exported for a later task's use and must not be assumed bulletproof
on an arbitrary unvalidated string.

Not fixed (reviewer's Minor 4, by direction): the default-config setup
call reaching api.openai.com with Authorization: Bearer not-required
when both base_url and api_key are left at defaults -- cosmetic, and
correctly surfaces as invalid_auth.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 18:08:20 -04:00
Adam SmithandClaude Sonnet 5 15889c905d fix: pin test_base_url.py entries to version 2 so migration is skipped
MockConfigEntry defaults to version=1, which triggers
async_migrate_integration on component setup; that legacy path
indexes entry.data[CONF_API_KEY] directly and raised KeyError for the
new optional-api-key entry in test_api_key_optional, before
async_setup_entry (and thus AsyncOpenAI) was ever reached. Pin both
entries in this file to version=2/minor_version=7, matching the
config flow's current VERSION/MINOR_VERSION and conftest's
mock_config_entry fixture.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:56:35 -04:00
Adam SmithandClaude Sonnet 5 df44abf158 feat: allow an empty API key for unauthenticated endpoints
The OpenAI SDK rejects api_key=None, so all three client sites now
fall back to a PLACEHOLDER_API_KEY ("not-required") when the entry has
no key, letting an unauthenticated LiteLLM/proxy endpoint be configured
with the API key field left blank.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:54:03 -04:00
Adam SmithandClaude Sonnet 5 914c89dc6b feat: normalize base_url and strip credentials from logs
New url_util.py (deliberately separate from the vendored files, so a
future upstream re-sync never conflicts with it): normalize_base_url
strips whitespace/trailing slashes and rejects non-http(s) or hostless
URLs; sanitize_url_for_logging masks embedded basic-auth credentials
for Task 7's discovery logging.

Wires normalization into config_flow.validate_input before the client
is built, with a new invalid_base_url form error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:53:10 -04:00
Adam SmithandClaude Sonnet 5 564c52159f feat: configurable base_url threaded to all three client sites
Adds CONF_BASE_URL/DEFAULT_BASE_URL and threads base_url through every
AsyncOpenAI construction site: config_flow.validate_input,
__init__.async_setup_entry, and config_flow's web-search geolocation
helper (_get_location_data) — the easy one to miss, since it would
otherwise silently talk to api.openai.com when the user configured a
different endpoint.

Fixes upstream-ported fixtures (conftest.mock_config_entry,
test_config_flow's inline MockConfigEntry/data-schema assertions) to
supply base_url now that the config-flow schema requires it.

No translations/en.json exists in this repo (only strings.json), so
only strings.json got the new "base_url" UI strings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:50:38 -04:00
Adam SmithandClaude Sonnet 5 8d2326d435 test: add ha-ffmpeg for the tts platform's import chain
custom_components/openai_compatible/tts.py imports homeassistant.components.tts,
which imports homeassistant.components.ffmpeg, which does
`from haffmpeg.core import HAFFmpeg` at module level. Because
async_forward_entry_setups imports all of PLATFORMS (ai_task, conversation,
stt, tts) before setting any of them up, this one missing package failed
config entry setup entirely (SETUP_ERROR) and cascaded into the bulk of the
previous run's 104 failures: "Agent ... not found" in test_conversation.py,
KeyError('ai_task') in test_ai_task.py, and entry_not_loaded aborts /
KeyError('step_id') in test_config_flow.py's subentry flow tests.

Pinned to core tag 2026.9.2's homeassistant/components/ffmpeg/manifest.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:43:32 -04:00
Adam SmithandClaude Sonnet 5 24be183143 test: add PyTurboJPEG for ai_task's camera import chain
homeassistant.components.ai_task.task imports camera, conversation, image and
media_source; camera/img_util.py does `from turbojpeg import TurboJPEG` at
module level. Pinned to core tag 2026.9.2's
homeassistant/components/camera/manifest.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:41:01 -04:00
Adam SmithandClaude Sonnet 5 ba534ac160 test: add core component requirements needed by the ported suite
The manifest declares dependencies: ["conversation"], and the ported tests
also exercise stt/tts. A full Home Assistant install resolves each core
component's own manifest.json requirements at runtime; bare pytest does not,
so CI failed with ModuleNotFoundError: No module named 'hassil' as soon as
entity.py's `from homeassistant.components import conversation` actually ran
(pythonpath fix in the prior commit got collection far enough to hit this).

Versions pinned to core tag 2026.9.2's own manifests:
homeassistant/components/conversation/manifest.json (hassil, gazetteer-matcher,
home-assistant-intents) and homeassistant/components/tts/manifest.json (mutagen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:38:57 -04:00
Adam SmithandClaude Sonnet 5 fa9648213d test: port core's openai_conversation suite as regression net
Ported from home-assistant/core tag 2026.9.2 (tests/components/openai_conversation):
conftest.py fixtures merged into tests/conftest.py, test_config_flow.py,
test_conversation.py and test_ai_task.py copied and rewritten for the fork's
custom_components.openai_compatible / "openai_compatible" domain.

Also copied as local test infrastructure (not core-only, self-contained):
- tests/__init__.py: the openai_conversation test package's own event-builder
  helpers (create_message_item, create_function_tool_call_item, etc.) that
  test_conversation.py and test_ai_task.py import via `from . import ...`.
- tests/snapshots/test_conversation.ambr: just the test_model_args snapshot
  entry, the one surviving syrupy-based assertion (see deletions below).

Deleted from test_conversation.py — depend on tests.components.conversation's
MockChatLog/mock_chat_log fixture, which lives in home-assistant/core's own
tests tree and is not distributable via pytest-homeassistant-custom-component
(confirmed: not present in the installed package's file list):
- test_function_call
- test_function_call_without_reasoning
- test_reasoning_summary_off_omits_summary_key
- test_web_search
- test_web_search_remove_citations_gpt5
- test_code_interpreter

Their now-unused imports (create_code_interpreter_item, create_web_search_item,
CONF_CODE_INTERPRETER, CONF_REASONING_SUMMARY, the CONF_WEB_SEARCH* constants,
freezegun.freeze_time, ToolInput, datetime) were removed with them.

`from tests.common import MockConfigEntry` (core's own tests package, also not
shipped by pytest-homeassistant-custom-component) rewritten to
`from pytest_homeassistant_custom_component.common import MockConfigEntry`,
matching the convention already used in this fork's tests/test_init.py.

Not run locally: this workstation is Python 3.13, HA 2026.9 (and
pytest-homeassistant-custom-component) require 3.14, so no correct local
interpreter exists. Validated with `python3 -m py_compile` only; CI
(actions/setup-python@3.14) is the real test run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:36:52 -04:00
Adam SmithandClaude Sonnet 5 852ddc0aeb fix: unblock CI collection, align ruff target, drop core-only quality_scale
- pyproject.toml: add pythonpath = ["."] so tests/ can import
  custom_components.openai_compatible (CI was red at collection without it)
- pyproject.toml: ruff target-version py313 -> py314, matching HA 2026.9's
  floor (already reflected in the CI workflow and requirements_test.txt)
- .gitignore: add .ruff_cache/
- remove custom_components/openai_compatible/quality_scale.yaml — a
  core-repo-only concept; the manifest no longer declares a quality scale,
  so the file asserted a tier this fork hasn't earned

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:30:58 -04:00
Adam SmithandClaude Sonnet 5 9d8c36f793 feat: vendor openai_conversation from core 2026.9.2, rename domain
Verbatim copy from home-assistant/core at tag 2026.9.2, with only the
domain constant and manifest changed. Apache-2.0, attribution in NOTICE.

Also fixed self-references the domain rename left dangling: the
config_entry selector's `integration:` field in services.yaml, the
`[%key:component::openai_conversation::...%]` translation
cross-references in strings.json, the deprecated-action descriptions
naming the old domain, and config_flow.py's instructions_url (now
pointing at this repo instead of the core docs page).

Local pytest run skipped per owner decision (see prior commit); files
validated with py_compile / json.load / yaml.safe_load instead. CI
(added in the prior commit) is the real gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:23:37 -04:00
Adam SmithandClaude Sonnet 5 178d948b1b chore: repo scaffold, Apache-2.0, test harness, CI workflows
Local pytest execution is skipped by owner decision: no release of
pytest-homeassistant-custom-component that pins a Home Assistant
2026.9.x floor installs on Python 3.13 (0.13.317+ requires Python
3.14, which this workstation does not have). requirements_test.txt
is pinned to the correct >=0.13.317 range for CI (Python 3.14) and
files are validated with py_compile instead of a local pytest run.

CI workflows (test.yaml, validate.yaml) moved forward from a later
task since, with local testing gone, CI is the only gate and must
exist before the code it gates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
2026-09-17 17:22:05 -04:00