mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
* nix: add `cage` to devShell
* test(desktop): add pre-filled sessions support
Exports createSandbox, writeMockProviderConfig, writeEnvFile,
buildAppEnv, findElectron, and launchDesktop from fixtures.ts so
specs can compose their own seeded-backend fixtures without duplicating
the sandbox/config/launch logic.
* test(desktop): auto-fail e2e tests on error banner
Adds a shared test fixture (e2e/test.ts) that wraps @playwright/test's
page with an error-banner guard. When any [role="alert"] element
(error notification toast) appears in the DOM during a test, the test
fails with the error message text.
The guard uses:
- A MutationObserver (injected via addInitScript) that watches for
[role="alert"] elements appearing at any point during the test
- A final DOM scan in afterEach for alerts still visible at teardown
- Deduplication so the same error text only fires once
All existing e2e specs updated to import { test, expect } from './test'
instead of '@playwright/test'. No per-spec setup needed — the guard is
auto-installed on every page via the extended fixture.
This catches issues like the "resume failed" error banner that can
appear during session loading — previously the test would pass while
an error toast was silently visible on screen.
* fix(state): parse tool_calls JSON string before re-serializing
_insert_message_rows and append_message both do json.dumps(tool_calls)
to serialize the field for SQLite storage. But when tool_calls arrives
as a JSON string (from import_sessions / export_session, which store it
as TEXT), json.dumps double-encodes it — wrapping the already-serialized
string in quotes and escaping the inner quotes.
When _rows_to_conversation later does json.loads(row['tool_calls']),
the double-encoded string parses back to a plain string (not a list).
_history_to_messages then iterates this string character-by-character,
calling tc.get('function', {}) on each char — 'str' object has no
attribute 'get'.
This was a pre-existing bug (on main), but only triggered by the
import_sessions path (the live agent always passes tool_calls as a
Python list). The e2e error-banner guard caught it via the 'Resume
failed' notification toast.
Fix: in both append_message and _insert_message_rows, parse tool_calls
with json.loads first if it's a string, then re-serialize.
* fix(desktop): exempt boot-failure from error guard
- boot-failure: add allowErrorBanners() beforeEach — these tests
deliberately trigger boot errors, so error toasts are expected
- test.ts: export allowErrorBanners() opt-out + reset flag in afterEach
56 lines
1.6 KiB
Python
56 lines
1.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Seed a Hermes state.db with a session exported from a real conversation.
|
|
|
|
Usage: seed_session_db.py <state_db_path> <fixture_json_path>
|
|
|
|
Creates the database with the full SessionDB schema (if it doesn't exist)
|
|
and imports the session from the JSON fixture. Uses the real
|
|
SessionDB.import_sessions() so the data shape matches what the desktop
|
|
backend expects.
|
|
"""
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add the repo root to sys.path so we can import hermes_state.
|
|
# The script is invoked from apps/desktop/e2e/ — repo root is ../../..
|
|
repo_root = Path(__file__).resolve().parents[3]
|
|
sys.path.insert(0, str(repo_root))
|
|
|
|
from hermes_state import SessionDB # noqa: E402
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 3:
|
|
print(f"Usage: {sys.argv[0]} <state_db_path> <fixture_json_path>", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
db_path = Path(sys.argv[1])
|
|
fixture_path = Path(sys.argv[2])
|
|
|
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(fixture_path, "r", encoding="utf-8") as f:
|
|
session_data = json.load(f)
|
|
|
|
db = SessionDB(db_path=db_path)
|
|
result = db.import_sessions([session_data])
|
|
|
|
if not result.get("ok"):
|
|
print(f"Import failed: {result}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
imported = result.get("imported", 0)
|
|
skipped = result.get("skipped", 0)
|
|
errors = result.get("errors", [])
|
|
|
|
if errors:
|
|
print(f"Import had errors: {errors}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(f"Seeded {imported} session(s), skipped {skipped} → {db_path}")
|
|
db.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|