adapter.py imports from gateway.*, which lives in the Hermes Agent rather
than this repo. Without its source on the PYTHONPATH, collection dies with
ModuleNotFoundError: No module named 'gateway' before a single test runs. So
fetch the Hermes Agent and keep it around before running the tests:
git clone https://github.com/NousResearch/hermes-agent.git /tmp/hermes
PYTHONPATH=/tmp/hermes uv run --with pyyaml pytest -q
gateway.config needs pyyaml, which is not among our dev dependencies. If a
checkout already exists somewhere, point at that path instead of cloning again.
Work against the declared types. Do not reach for values dynamically when a typed attribute exists.
chattolib ships dataclasses with py.typed for everything it returns —
Message, Asset, AssetUpload, Room, User. Read their fields directly, so
a wrong name is a loud AttributeError at the first call and a type checker can
see it before that:
upload_id = upload.upload_id # yes
upload_id = str(getattr(cast(Any, upload), "id", "")) # no
The second form cost us every single file upload. AssetUpload has no id
field — it is called upload_id — but cast(Any, ...) disables the check and
the "" default turns the mismatch into an empty string. The result was an
error message blaming the server (CreateUpload returned no upload ID) for a
name we got wrong ourselves, and it survived in production because it never
raised.
So: no cast(Any, ...) on a value whose type is known, and no getattr with a
fallback default to paper over a field you are not sure about — look the field up
in vendor/common/chattolib/types.py instead. getattr is fine for genuinely
optional or duck-typed things (a handler that may not be set, a payload from an
untyped source), not for dodging a type.
Tests should exercise real library types too, not mocks shaped like them. The
upload bug was invisible because the tests replaced _upload_asset wholesale
with an AsyncMock, so no test ever touched a real AssetUpload.
When you extend what the adapter can do, keep the startup banner current.
_capabilities() in adapter.py logs what the plugin supports at registration,
so starting it up tells you what is available instead of making you read the
source. Whether a capability counts is derived from real overrides: it only
counts when ChattoAdapter replaces the BasePlatformAdapter method. The label
for it, however, lives in _CAPABILITY_LABELS and is not discovered
automatically.
So when you add an overridden BasePlatformAdapter method:
_CAPABILITY_LABELS (method name → wording that means
something to someone who has not read the code). Skip this and the plugin can
do the thing but says so nowhere.platform_hint in register() needs to mention it. That
hint goes into the system prompt and is the only way the model learns what the
channel can do — without it the agent falls back to shelling out.TestRegistration if the capability is user-visible.The reverse holds too: drop a method and its line disappears from the log on its
own, but its _CAPABILITY_LABELS entry should go with it.
Always run shellcheck after editing any shell scripts.
shellcheck path/to/script.sh
Or validate all shell scripts in the project:
find . -name "*.sh" -exec shellcheck {} \;
Project-specific shellcheck rules are defined in .shellcheckrc. Default severity is error to catch all issues.
Consider adding a pre-commit hook for automatic validation:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.9.0
hooks:
- id: shellcheck
args: [--severity=error]