Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
8733f19
fix(loop): rewrite /loop as same-session heartbeat using continuation…
May 13, 2026
b1bb388
fix(loop): rewrite /loop as same-session heartbeat using continuation…
May 13, 2026
0342b57
fix: /loop — minimum 60s interval, no popup, immediate first tick
May 14, 2026
c358498
feat(gateway): add _dispatch_loop_prompt for adapter-free loop injection
May 14, 2026
f278d6b
fix(gateway): add daemon loop ticker with _dispatch_loop_prompt injec…
May 14, 2026
961f590
fix(gateway): use _dispatch_loop_prompt in post-turn loop continuation
May 14, 2026
4e019e8
test: update loop tests for _dispatch_loop_prompt and no on_message
May 14, 2026
40ea3a1
fix(loop): detect slash_worker mode in CLI, add daemon loop ticker to…
May 14, 2026
ba966c8
refactor(loop): unify LoopScheduler as single tick engine with dispat…
May 14, 2026
11f279b
fix(loop): add missing Callable import, fix shadowed key variable
May 14, 2026
5a268ad
fix(loop): start scheduler eagerly, keep polling when no loop in DB
May 14, 2026
8524354
refactor(loop): auto-UID multi-loop — no names, every /loop creates n…
May 14, 2026
332fcf7
feat(loop): live countdown — next tick time in /loop list/status
May 14, 2026
c90d0e3
fix(loop): double-fire on create + stale countdown in status
May 14, 2026
acee011
restore multi-provider memory support
May 17, 2026
056b764
feat: multi-provider memory, kanban, holographic, mem0, ha integratio…
May 12, 2026
52e3208
fix(kanban): migrate task session index after columns
quocanh261997 May 19, 2026
4c14b9c
fix(kanban): also hoist idx_events_run + drop redundant inner create
kshitijk4poor May 19, 2026
401b0f5
fix(compress): add missing abort_on_summary_failure param from upstre…
May 19, 2026
083b04e
fix: complete fork-addition restore — all 74 failures resolved
May 19, 2026
550ded5
fix(gateway): restore @staticmethod on _is_loop_continuation_event + …
May 19, 2026
1326fe2
fix: register /loop under Session category in COMMAND_REGISTRY
May 19, 2026
e8d3ab0
fix: LoopState.id None safety + backward compat; dispatch/fork schedu…
May 19, 2026
c5fd720
fix: verify and polish gateway restart-resume + memory perf skipif
May 19, 2026
f26f8c6
fix(commands): add codex_runtime alias for codex-runtime slash command
May 19, 2026
ebbe55c
fix(gateway): wire _handle_loop_command dispatch for /loop in gateway…
May 19, 2026
4a1dd95
fix: doctor hint ordering, quit --delete flag, google_chat checker, b…
May 19, 2026
6819127
fix(compress): add _last_compress_aborted attr, force kwarg, abort fl…
May 19, 2026
48a36bf
fix: restore CLI kanban auto-subscribe, fix double-sub with gateway h…
May 19, 2026
e15d7d1
fix: restore sys.stdout/stderr on partial _install_hangup_protection …
May 20, 2026
12f37c0
fix(test): use __qualname__ instead of isinstance for spawn-worker co…
May 20, 2026
6a343ef
fix(doctor): add missing _section helper function from upstream merge
May 20, 2026
c1487e5
fix(test): bump interrupt timing threshold from 1.0s to 2.0s for slow CI
May 20, 2026
bfed3a1
fix(test): prevent make_env finalizer from leaking snapshot state acr…
May 20, 2026
4b83aa0
fix: remove '-n 0' from integration test command — pytest-xdist not a…
May 24, 2026
94d1fd4
chore: refresh uv.lock after upstream rebase (4 new kanban commits)
May 24, 2026
e1daebb
fix(cli): rename undefined _format_process_notification to format_pro…
May 16, 2026
ae040a8
feat(kanban): event-driven TUI notification bridge via FIFO
May 16, 2026
510eece
docs: add TUI notification pipeline investigation report
May 16, 2026
0120ca4
test(kanban): add FIFO notification bridge tests
May 17, 2026
ce9d515
fix(process_registry): remove _completion_consumed.add from poll() + …
May 17, 2026
4a763ef
fix: remove stale 'poll' references from completion consumption docs/…
May 17, 2026
ebf60b6
fix: add TUI poller dedup test — verify mark_completion_consumed afte…
May 17, 2026
3f3f0ca
fix(webui): propagate contextvars for background process notifications
May 18, 2026
4878b59
fix(webui): propagate contextvars for background process notifications
May 18, 2026
0cc3537
test(notifications): add regression guard coverage for all notificati…
May 18, 2026
c6d54fa
fix(webui): propagate contextvars for background process notifications
May 18, 2026
e303b76
test(webui): add notification delivery tests for contextvar propagation
May 18, 2026
f271fa3
fix(tests): address verification findings for regression guards
May 18, 2026
6a37df0
fix(gateway): deliver background process notifications to WebUI via a…
May 18, 2026
c86cc79
fix(gateway): bridge api_server SSE delivery for background process n…
May 18, 2026
95823e7
fix(gateway): bridge api_server SSE delivery for background process n…
May 18, 2026
e00b103
fix(tests): make git history guard test robust — search all branches,…
May 20, 2026
f255d30
fix(kanban): add encoding=utf-8 to FIFO open() for Windows footgun co…
May 24, 2026
31519fc
fix(compression): include session_id in run_conversation result dict
May 24, 2026
9c7622a
fix(compression): also add should_compress() anti-thrashing to prefli…
May 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions $HERMES_KANBAN_WORKSPACE/INVESTIGATION-I2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Investigation I2: Google Chat tests — Platform.GOOGLE_CHAT missing (11 failures)

**Date:** 2026-05-08
**CI run:** 25574168674 — test (3.13)
**File:** `tests/gateway/test_google_chat.py` (2582 lines)

## Summary

`Platform.GOOGLE_CHAT` is not a built-in enum member. The `google_chat` platform exists only as a **bundled plugin** under `plugins/platforms/google_chat/`. The `Platform` enum's `_missing_()` method can dynamically create pseudo-members, but **only when called as `Platform("google_chat")`**. Attribute-style access (`Platform.GOOGLE_CHAT`) bypasses `_missing_()` entirely in Python's `Enum` implementation and raises `AttributeError`.

All 11 failing tests access `Platform.GOOGLE_CHAT` via attribute lookup before any `Platform("google_chat")` call has triggered `_missing_()`.

---

## Evidence

### 1. `Platform` enum does NOT list GOOGLE_CHAT as a built-in member

`gateway/config.py` lines 82-110:

```python
class Platform(Enum):
LOCAL = "local"
TELEGRAM = "telegram"
DISCORD = "discord"
WHATSAPP = "whatsapp"
SLACK = "slack"
SIGNAL = "signal"
MATTERMOST = "mattermost"
MATRIX = "matrix"
HOMEASSISTANT = "homeassistant"
EMAIL = "email"
SMS = "sms"
DINGTALK = "dingtalk"
API_SERVER = "api_server"
WEBHOOK = "webhook"
FEISHU = "feishu"
WECOM = "wecom"
WECOM_CALLBACK = "wecom_callback"
WEIXIN = "weixin"
BLUEBUBBLES = "bluebubbles"
QQBOT = "qqbot"
YUANBAO = "yuanbao"
# --- no GOOGLE_CHAT ---
```

### 2. The `google_chat` plugin exists

```
plugins/platforms/google_chat/
├── __init__.py
├── adapter.py
├── oauth.py
└── plugin.yaml
```

Commit that added it: `44cd79e79` — `feat(plugins/google_chat): Google Chat platform adapter as a bundled plugin`

The `Platform._scan_bundled_plugin_platforms()` method scans `plugins/platforms/` and discovers `google_chat` as a bundled plugin. `_missing_("google_chat")` would successfully create a pseudo-member.

### 3. `_missing_()` creates pseudo-members on VALUE lookup, not ATTRIBUTE lookup

`gateway/config.py` lines 111-154:

```python
@classmethod
def _missing_(cls, value):
"""Accept unknown platform names only for known plugin adapters.
Creates a pseudo-member cached in _value2member_map_ so that
Platform("irc") is Platform("irc") holds True.
"""
if not isinstance(value, str) or not value.strip():
return None
value = value.strip().lower()
if value in cls._value2member_map_:
return cls._value2member_map_[value]
# ... scan bundled plugins ...
if value in _Platform__bundled_plugin_names:
pseudo = object.__new__(cls)
pseudo._value_ = value
pseudo._name_ = value.upper().replace("-", "_").replace(" ", "_")
cls._value2member_map_[value] = pseudo
cls._member_map_[pseudo._name_] = pseudo
return pseudo
# ... runtime registry check ...
```

Key point: `Platform.GOOGLE_CHAT` does **not** call `_missing_()`. Python's `Enum.__getattr__` does not delegate to `_missing_`; it raises `AttributeError` if the name is not in `_member_map_`. Only `Platform("google_chat")` invokes `_missing_()`.

### 4. All failing tests use `Platform.GOOGLE_CHAT` attribute access

Filing tests from `tests/gateway/test_google_chat.py`:

| Test class | Line | Failure pattern |
|---|---|---|
| `TestPlatformRegistration::test_enum_value` | 232 | `assert Platform.GOOGLE_CHAT.value == "google_chat"` |
| `TestEnvConfigLoading` (8 tests) | 266-325 | `cfg.platforms[Platform.GOOGLE_CHAT]` repeated |
| `TestAuthorizationEmailMatch` (3 tests) | 2453+ | `Platform.GOOGLE_CHAT` in allowlist logic |

None of these tests call `Platform("google_chat")` first. Therefore `_missing_()` is never triggered, `_member_map_` never gains "GOOGLE_CHAT", and attribute access fails with `AttributeError`.

---

## Root Cause

The tests assume `GOOGLE_CHAT` is a static built-in enum member, but it is actually a **dynamic plugin member**. The `Platform` enum's dynamic member mechanism (`_missing_()`) only works for value-based lookups (`Platform("google_chat")`), not attribute lookups (`Platform.GOOGLE_CHAT`).

When the tests import `Platform` from `gateway.config`, `GOOGLE_CHAT` is absent from `_member_map_`. Because no test calls `Platform("google_chat")` before accessing the attribute, the pseudo-member is never created, and all 11 tests fail.

---

## Fix Options

### Option A: Add `GOOGLE_CHAT` as a built-in enum member (recommended)

Add one line to `gateway/config.py` in the `Platform` enum:

```python
YUANBAO = "yuanbao"
GOOGLE_CHAT = "google_chat"
```

This treats Google Chat the same as all other built-in platforms. It is the simplest fix and aligns with the test assumptions. The plugin adapter under `plugins/platforms/google_chat/` will still work; the built-in enum member and the plugin are not mutually exclusive.

### Option B: Change tests to use `Platform("google_chat")`

Replace every `Platform.GOOGLE_CHAT` in the tests with `Platform("google_chat")`. This would trigger `_missing_()` and create the pseudo-member. However, this is more invasive (many changes across 2582 lines) and makes the tests inconsistent with how all other platforms are referenced.

### Option C: Force early `_missing_()` call in test setup

Add a fixture or module-level call to `Platform("google_chat")` before attribute access. Fragile and non-obvious.

---

## Recommendation

**Option A** — add `GOOGLE_CHAT = "google_chat"` to the `Platform` enum in `gateway/config.py`.

Rationale:
- The Google Chat adapter is a real, committed feature (`44cd79e79`).
- It has a full test suite (2582 lines) and plugin scaffolding.
- All other shipped platforms are built-in enum members; `google_chat` should be too.
- One-line fix vs. ~30+ line changes across test file.
- No risk to plugin loader — `_missing_()` will still work, and the built-in member takes precedence.

---

## Files to Modify (if fix is approved)

```
gateway/config.py # add "GOOGLE_CHAT = \"google_chat\"" after YUANBAO
```

No test changes needed with Option A.
43 changes: 43 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,49 @@ jobs:
run: |
source .venv/bin/activate
python -m pytest tests/e2e/ -v --tb=short
env:
OPENROUTER_API_KEY: ""
OPENAI_API_KEY: ""
NOUS_API_KEY: ""

integration:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
python-version: ['3.11', '3.12', '3.13']
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y ripgrep libopus0

- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5

- name: Cache uv
uses: actions/cache@v4
with:
path: ~/.cache/uv
key: ${{ runner.os }}-uv-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml', 'uv.lock') }}
restore-keys: |
${{ runner.os }}-uv-${{ matrix.python-version }}-
${{ runner.os }}-uv-

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install dependencies
run: |
uv venv .venv --python ${{ matrix.python-version }}
source .venv/bin/activate
uv pip install -e ".[all,dev]"

- name: Run integration tests
run: |
source .venv/bin/activate
python -m pytest tests/integration/ -v --tb=short -m integration
env:
OPENROUTER_API_KEY: ""
OPENAI_API_KEY: ""
Expand Down
160 changes: 160 additions & 0 deletions .hermes/plans/2026-05-08-plan-I2-google-chat-enum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Plan I2: Google Chat Platform Enum — Implementation Plan

> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.

**Goal:** Add `GOOGLE_CHAT = "google_chat"` as a built-in enum member to the `Platform` enum in `gateway/config.py`, fixing all 11 Google Chat tests that fail with `AttributeError: GOOGLE_CHAT`.

**Architecture:** The `google_chat` adapter exists as a bundled plugin under `plugins/platforms/google_chat/`. The `Platform` enum supports dynamic pseudo-members via `_missing_()`, but that only works for value-based lookups (`Platform("google_chat")`) — not attribute lookups (`Platform.GOOGLE_CHAT`). All 11 failing tests use attribute access. Adding a built-in member makes Google Chat consistent with every other shipped platform.

**Tech Stack:** Python 3.13, pytest, hermes-agent `gateway/config.py`

---

## Verified State (Do Not Re-verify Root Cause)

The following is already correct and verified (parent task t_f071df92, run 898). Treat as ground truth.

### Defect: `gateway/config.py` lacks `GOOGLE_CHAT` in fork

The **default checkout** (`/home/d/.hermes/hermes-agent/gateway/config.py` lines 111-112):

```python
YUANBAO = "yuanbao"
GOOGLE_CHAT = "google_chat"
```

The **fork checkout** (`/home/d/Desktop/agenda/hermes-agent/gateway/config.py` line 110-111):

```python
YUANBAO = "yuanbao"
@classmethod
```

`GOOGLE_CHAT` is **absent** in the fork. This is the only delta causing the 11 test failures.

### Plugin Exists (Independent of Enum)

```
plugins/platforms/google_chat/
├── __init__.py
├── adapter.py
├── oauth.py
└── plugin.yaml
```

Commit `44cd79e79` added the plugin. It works regardless of whether `GOOGLE_CHAT` is a built-in enum member or a dynamic pseudo-member.

### Failing Tests (11)

| Test class | Count | Failure pattern |
|---|---|---|
| `TestPlatformRegistration` | 1 | `Platform.GOOGLE_CHAT.value == "google_chat"` |
| `TestEnvConfigLoading` | 8 | `cfg.platforms[Platform.GOOGLE_CHAT]` repeated |
| `TestAuthorizationEmailMatch` | 3 | `Platform.GOOGLE_CHAT` in allowlist logic |

---

## Task 1: Add `GOOGLE_CHAT` Built-in Enum Member

**Objective:** Insert the missing enum member into the fork's `gateway/config.py`.

**Files:**
- Modify: `gateway/config.py` (fork checkout)

**Step 1: Edit `gateway/config.py`**

Locate the `Platform` enum (line 82 in fork). After `YUANBAO = "yuanbao"` (line 110), add:

```python
GOOGLE_CHAT = "google_chat"
```

The enum block should then read:

```python
BLUEBUBBLES = "bluebubbles"
QQBOT = "qqbot"
YUANBAO = "yuanbao"
GOOGLE_CHAT = "google_chat"
@classmethod
def _missing_(cls, value):
```

**Step 2: Verify the edit**

```bash
cd /home/d/Desktop/agenda/hermes-agent
grep -n "GOOGLE_CHAT" gateway/config.py
```

Expected output:
```
111: GOOGLE_CHAT = "google_chat"
```

**Step 3: Stage (do not commit)**

```bash
cd /home/d/Desktop/agenda/hermes-agent
git add gateway/config.py
```

---

## Task 2: Run Google Chat Tests

**Objective:** Confirm the one-line fix resolves all 11 failures.

**Files:**
- Read-only: `tests/gateway/test_google_chat.py`

**Step 1: Run the full Google Chat test module**

```bash
cd /home/d/Desktop/agenda/hermes-agent
source .venv/bin/activate
pytest tests/gateway/test_google_chat.py -v --tb=short
```

**Expected output:**
```
tests/gateway/test_google_chat.py::TestPlatformRegistration::test_enum_value PASSED
tests/gateway/test_google_chat.py::TestEnvConfigLoading::test_load_empty_config PASSED
tests/gateway/test_google_chat.py::TestEnvConfigLoading::test_load_basic_config PASSED
... (8 more) ...
tests/gateway/test_google_chat.py::TestAuthorizationEmailMatch::test_email_exact_match PASSED
tests/gateway/test_google_chat.py::TestAuthorizationEmailMatch::test_email_domain_match PASSED
tests/gateway/test_google_chat.py::TestAuthorizationEmailMatch::test_email_no_match PASSED

11 passed in X.XXs
```

**Step 2: If any test fails**

Do not proceed. Run `git diff` to confirm the change is exactly the one line above. If tests still fail, report findings in a `kanban_comment` and `kanban_block` the task.

---

## Notes for Downstream Implementers

### Why only the fork is affected

The default checkout already contains `GOOGLE_CHAT = "google_chat"` (added in a prior task, t_69064a6f). The fork's `gateway/config.py` diverged and lost this line, likely due to an incomplete cherry-pick or merge resolution. The fix is strictly a forward-port of the missing line.

### Why `_missing_()` is not sufficient

Python's `Enum.__getattr__` does **not** delegate to `_missing_()` for attribute access. `_missing_()` is only invoked for value lookups like `Platform("google_chat")`. Because every failing test uses `Platform.GOOGLE_CHAT`, the pseudo-member mechanism is never triggered.

### Why no test changes are needed

The tests correctly assume all shipped platforms are built-in enum members. The defect is in the enum definition, not the tests.

---

## Files Referenced

| File | Role | Lines |
|---|---|---|
| `gateway/config.py` | Fix target | 110-111 (fork) |
| `tests/gateway/test_google_chat.py` | Verification | 2582 lines total |
| `plugins/platforms/google_chat/plugin.yaml` | Plugin metadata | — |
Loading