Skip to content

test: add unit tests for run_agent.py (AIAgent) - #67

Merged
teknium1 merged 1 commit into
NousResearch:mainfrom
0xbyt4:test/add-run-agent-unit-tests
Feb 27, 2026
Merged

test: add unit tests for run_agent.py (AIAgent)#67
teknium1 merged 1 commit into
NousResearch:mainfrom
0xbyt4:test/add-run-agent-unit-tests

Conversation

@0xbyt4

@0xbyt4 0xbyt4 commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add 71 unit tests for run_agent.py (AIAgent class) covering 16 methods across 3 groups
  • Pure functions (26 tests): _has_content_after_think_block, _strip_think_blocks, _extract_reasoning, _clean_session_content, _get_messages_up_to_last_assistant, _mask_api_key_for_logs
  • State/structure methods (33 tests): __init__, interrupt mechanism, _hydrate_todo_store, _build_system_prompt, _invalidate_system_prompt, _build_api_kwargs, _build_assistant_message, _format_tools_for_system_message
  • Conversation loop pieces (12 tests): _execute_tool_calls, _handle_max_iterations, run_conversation (stop, tool calls, interrupt, invalid tool retry, empty content fallback, context compression)

Test plan

  • uv run pytest tests/test_run_agent.py -v — 71 passed in ~3s
  • uv run pytest -v — full suite 243 passed, no regressions

71 tests covering pure functions, state/structure methods, and
conversation loop pieces. OpenAI client and tool loading are mocked.
@teknium1
teknium1 merged commit 64eca85 into NousResearch:main Feb 27, 2026
angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 27, 2026
…t-tests

test: add unit tests for run_agent.py (AIAgent)
benbarclay added a commit that referenced this pull request Jun 25, 2026
A DM reply carries no guild_id, so the connector's egress guard cannot
resolve the owning tenant from metadata.guild_id and declines the send
with "discord egress declined: target not routed to an onboarded tenant"
— the bug behind "the bot never replies in DMs". Guild replies are
unaffected (they carry guild_id), which is why the guild path worked
end-to-end while DMs looked broken.

The connector now resolves a DM reply's tenant from the recipient's
author binding (gateway-gateway #67, resolveByUser keyed on
metadata.user_id) — the outbound counterpart to inbound Phase 7a
author-first resolution. But it needs the recipient user_id ON the
outbound action, and the adapter only re-attached guild_id
(_capture_scope/_with_scope), no-op for DMs (the docstring even said so).

This extends the adapter's inbound-scope capture: for a DM (no guild_id)
remember chat_id -> the authentic author user_id we observed, and
re-attach it as metadata.user_id on outbound. Guild capture is unchanged
and wins when present; user_id is the DM-only fallback. The id is the one
the connector observed inbound (never gateway-asserted), so the trust
invariant holds.

+4 unit tests (DM reply re-attaches user_id + no guild_id; unknown chat
invents nothing; explicit user_id preserved; guild reply never carries
user_id). Proved load-bearing (reverting the re-attach fails the DM
test). 144 relay tests pass, ruff clean.

Pairs with gateway-gateway #67 (the connector-side resolver). Together
they close the DM-reply egress gap end-to-end.
pai-scaffolde referenced this pull request in pai-scaffolde/hermes-agent Jun 28, 2026
A DM reply carries no guild_id, so the connector's egress guard cannot
resolve the owning tenant from metadata.guild_id and declines the send
with "discord egress declined: target not routed to an onboarded tenant"
— the bug behind "the bot never replies in DMs". Guild replies are
unaffected (they carry guild_id), which is why the guild path worked
end-to-end while DMs looked broken.

The connector now resolves a DM reply's tenant from the recipient's
author binding (gateway-gateway #67, resolveByUser keyed on
metadata.user_id) — the outbound counterpart to inbound Phase 7a
author-first resolution. But it needs the recipient user_id ON the
outbound action, and the adapter only re-attached guild_id
(_capture_scope/_with_scope), no-op for DMs (the docstring even said so).

This extends the adapter's inbound-scope capture: for a DM (no guild_id)
remember chat_id -> the authentic author user_id we observed, and
re-attach it as metadata.user_id on outbound. Guild capture is unchanged
and wins when present; user_id is the DM-only fallback. The id is the one
the connector observed inbound (never gateway-asserted), so the trust
invariant holds.

+4 unit tests (DM reply re-attaches user_id + no guild_id; unknown chat
invents nothing; explicit user_id preserved; guild reply never carries
user_id). Proved load-bearing (reverting the re-attach fails the DM
test). 144 relay tests pass, ruff clean.

Pairs with gateway-gateway #67 (the connector-side resolver). Together
they close the DM-reply egress gap end-to-end.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…t-tests

test: add unit tests for run_agent.py (AIAgent)
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
A DM reply carries no guild_id, so the connector's egress guard cannot
resolve the owning tenant from metadata.guild_id and declines the send
with "discord egress declined: target not routed to an onboarded tenant"
— the bug behind "the bot never replies in DMs". Guild replies are
unaffected (they carry guild_id), which is why the guild path worked
end-to-end while DMs looked broken.

The connector now resolves a DM reply's tenant from the recipient's
author binding (gateway-gateway NousResearch#67, resolveByUser keyed on
metadata.user_id) — the outbound counterpart to inbound Phase 7a
author-first resolution. But it needs the recipient user_id ON the
outbound action, and the adapter only re-attached guild_id
(_capture_scope/_with_scope), no-op for DMs (the docstring even said so).

This extends the adapter's inbound-scope capture: for a DM (no guild_id)
remember chat_id -> the authentic author user_id we observed, and
re-attach it as metadata.user_id on outbound. Guild capture is unchanged
and wins when present; user_id is the DM-only fallback. The id is the one
the connector observed inbound (never gateway-asserted), so the trust
invariant holds.

+4 unit tests (DM reply re-attaches user_id + no guild_id; unknown chat
invents nothing; explicit user_id preserved; guild reply never carries
user_id). Proved load-bearing (reverting the re-attach fails the DM
test). 144 relay tests pass, ruff clean.

Pairs with gateway-gateway NousResearch#67 (the connector-side resolver). Together
they close the DM-reply egress gap end-to-end.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
A DM reply carries no guild_id, so the connector's egress guard cannot
resolve the owning tenant from metadata.guild_id and declines the send
with "discord egress declined: target not routed to an onboarded tenant"
— the bug behind "the bot never replies in DMs". Guild replies are
unaffected (they carry guild_id), which is why the guild path worked
end-to-end while DMs looked broken.

The connector now resolves a DM reply's tenant from the recipient's
author binding (gateway-gateway NousResearch#67, resolveByUser keyed on
metadata.user_id) — the outbound counterpart to inbound Phase 7a
author-first resolution. But it needs the recipient user_id ON the
outbound action, and the adapter only re-attached guild_id
(_capture_scope/_with_scope), no-op for DMs (the docstring even said so).

This extends the adapter's inbound-scope capture: for a DM (no guild_id)
remember chat_id -> the authentic author user_id we observed, and
re-attach it as metadata.user_id on outbound. Guild capture is unchanged
and wins when present; user_id is the DM-only fallback. The id is the one
the connector observed inbound (never gateway-asserted), so the trust
invariant holds.

+4 unit tests (DM reply re-attaches user_id + no guild_id; unknown chat
invents nothing; explicit user_id preserved; guild reply never carries
user_id). Proved load-bearing (reverting the re-attach fails the DM
test). 144 relay tests pass, ruff clean.

Pairs with gateway-gateway NousResearch#67 (the connector-side resolver). Together
they close the DM-reply egress gap end-to-end.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
A DM reply carries no guild_id, so the connector's egress guard cannot
resolve the owning tenant from metadata.guild_id and declines the send
with "discord egress declined: target not routed to an onboarded tenant"
— the bug behind "the bot never replies in DMs". Guild replies are
unaffected (they carry guild_id), which is why the guild path worked
end-to-end while DMs looked broken.

The connector now resolves a DM reply's tenant from the recipient's
author binding (gateway-gateway NousResearch#67, resolveByUser keyed on
metadata.user_id) — the outbound counterpart to inbound Phase 7a
author-first resolution. But it needs the recipient user_id ON the
outbound action, and the adapter only re-attached guild_id
(_capture_scope/_with_scope), no-op for DMs (the docstring even said so).

This extends the adapter's inbound-scope capture: for a DM (no guild_id)
remember chat_id -> the authentic author user_id we observed, and
re-attach it as metadata.user_id on outbound. Guild capture is unchanged
and wins when present; user_id is the DM-only fallback. The id is the one
the connector observed inbound (never gateway-asserted), so the trust
invariant holds.

+4 unit tests (DM reply re-attaches user_id + no guild_id; unknown chat
invents nothing; explicit user_id preserved; guild reply never carries
user_id). Proved load-bearing (reverting the re-attach fails the DM
test). 144 relay tests pass, ruff clean.

Pairs with gateway-gateway NousResearch#67 (the connector-side resolver). Together
they close the DM-reply egress gap end-to-end.
israellot added a commit to YallaPlay/hermes-agent that referenced this pull request Jul 20, 2026
Adopt upstream's fix (c9c9bb3): the fallback test must also patch
hermes_cli.model_catalog.get_curated_openrouter_models, otherwise the
assertion depends on whatever the deployed remote manifest currently
contains — which drifted and broke CI on main (runs NousResearch#66/NousResearch#67).
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
A DM reply carries no guild_id, so the connector's egress guard cannot
resolve the owning tenant from metadata.guild_id and declines the send
with "discord egress declined: target not routed to an onboarded tenant"
— the bug behind "the bot never replies in DMs". Guild replies are
unaffected (they carry guild_id), which is why the guild path worked
end-to-end while DMs looked broken.

The connector now resolves a DM reply's tenant from the recipient's
author binding (gateway-gateway NousResearch#67, resolveByUser keyed on
metadata.user_id) — the outbound counterpart to inbound Phase 7a
author-first resolution. But it needs the recipient user_id ON the
outbound action, and the adapter only re-attached guild_id
(_capture_scope/_with_scope), no-op for DMs (the docstring even said so).

This extends the adapter's inbound-scope capture: for a DM (no guild_id)
remember chat_id -> the authentic author user_id we observed, and
re-attach it as metadata.user_id on outbound. Guild capture is unchanged
and wins when present; user_id is the DM-only fallback. The id is the one
the connector observed inbound (never gateway-asserted), so the trust
invariant holds.

+4 unit tests (DM reply re-attaches user_id + no guild_id; unknown chat
invents nothing; explicit user_id preserved; guild reply never carries
user_id). Proved load-bearing (reverting the re-attach fails the DM
test). 144 relay tests pass, ruff clean.

Pairs with gateway-gateway NousResearch#67 (the connector-side resolver). Together
they close the DM-reply egress gap end-to-end.
Meraniya pushed a commit to Meraniya/hermes-agent that referenced this pull request Aug 6, 2026
…nting (NousResearch#67)

* feat(devops): add config-integrity-watchdog skill with git-backed fingerprinting

Replaces mutable .sha256 sidecar with an append-only integrity log
committed to the dotfiles git repo. A process without git commit
credentials cannot silently forge a fingerprint entry.

- seal.py: hash config + append to log + git commit
- verify.py: compare current hash against latest seal; detect log tampering
- restore.py: revert from git + re-seal; backs up tampered config
- PLAN.md: project plan (Linear not available)
- Tests: seal->verify, seal->tamper->verify, seal->tamper->restore->verify (29 passing)

Closes the 19-day recurring config hijack pattern (Event NousResearch#25+).
Slack: https://mfc-nyc.slack.com/archives/C0BD8QBUSJF/p1782742870774319

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh

* fix(tests): add type ignore for pytest import and None guard for regex match

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh

* feat(cli): add hermes config seal/verify/restore commands

Integrates config-integrity-watchdog into the Hermes CLI so users
can seal, verify, and restore config integrity without remembering
script paths. Calls core logic shared with the standalone scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh

* fix(cli): fallback import path, move PLAN.md to .plans/, add restore caveat

- _import_core() now tries ~/.hermes/skills first, then repo-relative
  skills/ as fallback for pre-sync usage; prints clear error + exits 1
  if skill not found in either location
- Move PLAN.md to .plans/config-integrity-watchdog.md
- Append git-HEAD caveat to restore.py module docstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh

---------

Co-authored-by: Claude <noreply@anthropic.com>
Meraniya pushed a commit to Meraniya/hermes-agent that referenced this pull request Aug 6, 2026
…, NousResearch#69) and mark arc complete (NousResearch#76)

Session close-out audit found the Outcome section only mentioned PR NousResearch#67.
PR NousResearch#69 explicitly self-describes as "follow-up to NousResearch#67" (unsigned-commit
git fallback) and PR NousResearch#68 (AGENTS.md docs) is also a direct follow-up;
neither was recorded. Also clarifies that NousResearch#71/NousResearch#72 (cron ticker
heartbeat/stall fix) are an unrelated arc shipped the same day, not
part of this project.


Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh

Co-authored-by: Claude <noreply@anthropic.com>
Meraniya pushed a commit to Meraniya/hermes-agent that referenced this pull request Aug 6, 2026
…writes (NousResearch#82)

save_config()/restore_config() already keep the local .sha256 sidecar in
sync (PR NousResearch#57), but the *external* git-backed baseline used by
`hermes config verify` (PR NousResearch#67) was only ever updated by an explicit
`hermes config seal`. Any authorized write through save_config() — model
scanner, /model command, platform setup flows via
write_platform_config_field() — desynced that baseline, so the Config
Integrity Watchdog cron job flagged the legitimate change as tampering:
"Hermes Config Integrity Failure! The configuration hash does not match
the sealed baseline." This is the same scanner/watchdog TOCTOU conflict
documented in docs/plans/2026-07-02-scanner-watchdog-conflict-resolution.md,
just recurring one layer over in the newer git-backed mechanism.

_write_config_to_disk() now also calls the config-integrity-watchdog
skill's seal() (quietly) whenever $HERMES_DOTFILES_DIR is configured,
keeping the git-backed log current on every authorized write. Extracted
_find_core_module() (returns None instead of exiting) so this can
opportunistically no-op on machines without the watchdog set up.
restore_quick_snapshot() in backup.py gets the same treatment for
config.yaml restores from a quick snapshot.

Co-authored-by: Test <test@test.com>
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
A DM reply carries no guild_id, so the connector's egress guard cannot
resolve the owning tenant from metadata.guild_id and declines the send
with "discord egress declined: target not routed to an onboarded tenant"
— the bug behind "the bot never replies in DMs". Guild replies are
unaffected (they carry guild_id), which is why the guild path worked
end-to-end while DMs looked broken.

The connector now resolves a DM reply's tenant from the recipient's
author binding (gateway-gateway NousResearch#67, resolveByUser keyed on
metadata.user_id) — the outbound counterpart to inbound Phase 7a
author-first resolution. But it needs the recipient user_id ON the
outbound action, and the adapter only re-attached guild_id
(_capture_scope/_with_scope), no-op for DMs (the docstring even said so).

This extends the adapter's inbound-scope capture: for a DM (no guild_id)
remember chat_id -> the authentic author user_id we observed, and
re-attach it as metadata.user_id on outbound. Guild capture is unchanged
and wins when present; user_id is the DM-only fallback. The id is the one
the connector observed inbound (never gateway-asserted), so the trust
invariant holds.

+4 unit tests (DM reply re-attaches user_id + no guild_id; unknown chat
invents nothing; explicit user_id preserved; guild reply never carries
user_id). Proved load-bearing (reverting the re-attach fails the DM
test). 144 relay tests pass, ruff clean.

Pairs with gateway-gateway NousResearch#67 (the connector-side resolver). Together
they close the DM-reply egress gap end-to-end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants