mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
* test(desktop): e2e test for interim assistant message preservation (#65919) Adds a Playwright E2E test that reproduces the fix from PR #65919 across all three layers (agent core → tui_gateway → desktop renderer). The mock inference server is upgraded with a multi-turn scripted response that exercises several interleaved patterns: 1. text + tool_call → should produce an interim message 2. text + tool_call → another interim message 3. no text + tool_call → NO interim (no visible text alongside tools) 4. text + tool_call → another interim message 5. final answer (stop) → message.complete, different from all interims Two describe blocks exercise display.interim_assistant_messages both on (default) and off: - ON: all interim texts + the final answer visible in the transcript - OFF: only the final answer visible, all interim texts wiped Also fixes a footgun: test:e2e now runs `npm run build` as a pretest hook so the renderer dist/ is always fresh. Previously, running `npx playwright test` locally would silently load a stale dist/ that predated renderer fixes — the python backend ran from source (had the fix) but the renderer was frozen in an old bundle. CI already built fresh, so the explicit build step there is removed to avoid duplication. * test(desktop): e2e sidebar states — background dot, subagent, cross-session Add sidebar-states.spec.ts with three E2E tests exercising the desktop sidebar's session dot states driven by real gateway events: 1. Background process dot appears during a terminal(background=true) call and disappears after auto-dismiss; subagent (delegate_task) runs concurrently; final answer is visible in the transcript. 2. Background dot remains visible while a subagent runs concurrently (longer sleep 5 background process so the dot is catchable). 3. Cross-session dot transition: start a turn with a background process, wait for the turn to complete, open a new session, then verify the original session's dot transitions from 'background running' to 'finished — unread' when the background process exits. The mock server gains SIDEBAR_SCRIPT and SIDEBAR_CROSS_SCRIPT trigger keywords that return tool_calls for terminal(background=true) and delegate_task — the agent executes these for real (real background process, real subagent), so the tests assert against genuine gateway events rather than mocked UI state. Verified: 3 passed (1.2m) under cage headless wlroots. * test(desktop): e2e tests for tile-unread bug (tab passes, split fails) Two scenarios for the tile-unread bug where a session that finishes while visible on-screen gets the green 'finished unread' dot even though the user is looking right at it. The unread check in handleTransition (session-states.ts:174) only compares against $selectedStoredSessionId and ignores $sessionTiles, so a session visible in a tile gets marked unread even though it's on screen. 1. TAB (hidden, PASSES): ⌃-click opens the session as a stacked tab that is NOT visible on screen. The unread dot IS correct here — the user isn't looking at it. 2. SPLIT (visible, FAILS): drag the session row to the workspace's right edge to create a side-by-side split tile. Both sessions are visible on screen. The unread dot is WRONG — the session is visible in the split tile, so it should not be marked 'unread'. This test is RED until the fix lands. Also adds explicit page.screenshot() calls at key assertion points in sidebar-states.spec.ts so the trace viewer has full-res captures of the sidebar dot states during the test. * test(desktop): cover compression and queued stop lifecycle Add real desktop E2E coverage for session compression continuation and queue parking after an explicit Stop. Extend the mock server with a blocking scripted turn and submitted-prompt assertions. * test(desktop): cover busy composer submit routing Replace the invalid queued-stop E2E scenario: plain text redirects a busy turn rather than entering the queue. Add focused submit-routing coverage for plain text, slash commands, attachments, explicit Stop, and idle submission.
215 lines
10 KiB
YAML
215 lines
10 KiB
YAML
name: E2E Desktop
|
|
|
|
on:
|
|
workflow_call:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: e2e-desktop-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
e2e:
|
|
name: Playwright E2E (Linux)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
# ── System deps for Electron on headless Ubuntu ───────────────────
|
|
# Electron needs GTK, NSS,atk, etc. even under xvfb. Playwright's
|
|
# install-deps covers browsers; for Electron we install the apt
|
|
# packages directly.
|
|
- name: Install system dependencies for Electron
|
|
run: |
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq \
|
|
xvfb \
|
|
libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 \
|
|
xdg-utils libatspi2.0-0 libdrm2 libgbm1 libasound2t64
|
|
|
|
# ── Node ───────────────────────────────────────────────────────────
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
# Full npm ci (not --ignore-scripts): electron's postinstall
|
|
# downloads the binary we launch, and node-pty's native build is
|
|
# needed for the terminal pane.
|
|
- uses: ./.github/actions/retry
|
|
with:
|
|
command: npm ci
|
|
|
|
# ── Python (for the hermes serve backend) ──────────────────────────
|
|
- name: Install uv
|
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # 8.2.0
|
|
with:
|
|
enable-cache: true
|
|
cache-dependency-glob: |
|
|
pyproject.toml
|
|
uv.lock
|
|
- name: Set up Python 3.11
|
|
run: uv python install 3.11
|
|
- name: Install Python dependencies
|
|
uses: ./.github/actions/retry
|
|
with:
|
|
command: uv sync --locked --python 3.11 --extra all --extra dev
|
|
|
|
# ── Build desktop app ─────────────────────────────────────────────
|
|
# The Playwright step below runs `npm run build` before testing so
|
|
# dist/ is always fresh — no separate build step needed here.
|
|
|
|
# ── Restore visual baseline screenshots from main ──────────────────
|
|
# Baselines are generated on main (via --update-snapshots) and cached.
|
|
# On PRs, we restore them so toHaveScreenshot has something to compare
|
|
# against. The cache key is keyed on the desktop source files so a
|
|
# UI change naturally invalidates it — but we fall back to the main
|
|
# cache to avoid cold starts on unrelated PRs.
|
|
- name: Restore visual baseline screenshots
|
|
id: restore-baselines
|
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
|
with:
|
|
path: apps/desktop/e2e/*-snapshots
|
|
key: visual-baselines-${{ github.ref_name }}
|
|
restore-keys: |
|
|
visual-baselines-main
|
|
|
|
# ── Run Playwright E2E under xvfb ─────────────────────────────────
|
|
# xvfb runs at a fixed 1280x1024 screen so the 1220x800 Electron
|
|
# window always has a consistent viewport for screenshot comparison.
|
|
# On main, we run with --update-snapshots to generate baselines.
|
|
# `npm run test:e2e` builds dist/ as a pretest hook so the renderer
|
|
# is always fresh — no separate build step needed.
|
|
- name: Run Playwright E2E tests
|
|
working-directory: apps/desktop
|
|
run: |
|
|
if [ "${{ github.ref_name }}" = "main" ]; then
|
|
echo "On main — generating/updating baseline screenshots"
|
|
npm run build && xvfb-run -a --server-args="-screen 0 1280x1024x24" \
|
|
npx playwright test --reporter=list --update-snapshots
|
|
else
|
|
echo "On PR — comparing against cached baselines"
|
|
npm run build && xvfb-run -a --server-args="-screen 0 1280x1024x24" \
|
|
npx playwright test --reporter=list
|
|
fi
|
|
env:
|
|
CI: "true"
|
|
# Ensure no real API keys leak into the test env.
|
|
OPENROUTER_API_KEY: ""
|
|
OPENAI_API_KEY: ""
|
|
NOUS_API_KEY: ""
|
|
|
|
# ── Save updated baselines to cache (main only) ───────────────────
|
|
- name: Save updated baselines to cache
|
|
if: github.ref_name == 'main' && always()
|
|
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
|
with:
|
|
path: apps/desktop/e2e/*-snapshots
|
|
key: visual-baselines-main
|
|
|
|
# ── Upload Playwright report (HTML + traces) ──────────────────────
|
|
- name: Upload Playwright report
|
|
id: upload-report
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: playwright-report-${{ github.sha }}
|
|
path: apps/desktop/playwright-report
|
|
retention-days: 14
|
|
overwrite: true
|
|
|
|
# ── Upload test results (screenshots, traces, diffs) ───────────────
|
|
- name: Upload test results
|
|
id: upload-results
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: playwright-test-results-${{ github.sha }}
|
|
path: apps/desktop/test-results
|
|
retention-days: 14
|
|
overwrite: true
|
|
|
|
# ── Upload just the visual diffs (small, fast to review) ──────────
|
|
- name: Upload visual diffs
|
|
id: upload-diffs
|
|
if: always() && github.ref_name != 'main'
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: visual-diffs-${{ github.sha }}
|
|
path: |
|
|
apps/desktop/test-results/**/*-diff.png
|
|
apps/desktop/test-results/**/*-actual.png
|
|
apps/desktop/test-results/**/*-expected.png
|
|
retention-days: 14
|
|
overwrite: true
|
|
if-no-files-found: ignore
|
|
|
|
# ── Generate step summary with visual diff info ───────────────────
|
|
# Parse the JSON report + scan for diff images, then post a summary
|
|
# to the GitHub Actions step output so reviewers can see what changed
|
|
# without downloading artifacts. Runs AFTER uploads so it can link
|
|
# the artifact download URLs from their step outputs.
|
|
- name: Generate visual diff summary
|
|
if: always()
|
|
working-directory: apps/desktop
|
|
env:
|
|
REPORT_URL: ${{ steps.upload-report.outputs.artifact-url }}
|
|
RESULTS_URL: ${{ steps.upload-results.outputs.artifact-url }}
|
|
DIFFS_URL: ${{ steps.upload-diffs.outputs.artifact-url }}
|
|
run: |
|
|
echo "## Desktop E2E — Visual Diff Report" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
# Count diff images (playwright writes *-diff.png on mismatch)
|
|
DIFF_COUNT=$(find test-results -name '*-diff.png' 2>/dev/null | wc -l)
|
|
ACTUAL_COUNT=$(find test-results -name '*-actual.png' 2>/dev/null | wc -l)
|
|
|
|
if [ "$DIFF_COUNT" -eq 0 ]; then
|
|
echo "✅ All $ACTUAL_COUNT screenshot(s) matched their baselines (or no baselines existed yet)." >> "$GITHUB_STEP_SUMMARY"
|
|
else
|
|
echo "📸 **$DIFF_COUNT of $ACTUAL_COUNT screenshot(s) differ from baseline:**" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "| Test | Diff | Actual | Expected |" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "|------|------|--------|----------|" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
# List each diff image with a link to the artifact
|
|
for diff in $(find test-results -name '*-diff.png' 2>/dev/null | sort); do
|
|
base=$(echo "$diff" | sed 's/-diff\.png$//')
|
|
test_name=$(basename "$base")
|
|
echo "| $test_name | [diff]($diff) | [actual](${base}-actual.png) | [expected](${base}-expected.png) |" >> "$GITHUB_STEP_SUMMARY"
|
|
done
|
|
fi
|
|
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "📥 **Artifacts:**" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
if [ -n "$RESULTS_URL" ]; then
|
|
echo "- [playwright-test-results]($RESULTS_URL) — all screenshots (actual + expected + diff) + traces" >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
if [ -n "$REPORT_URL" ]; then
|
|
echo "- [playwright-report]($REPORT_URL) — interactive HTML report" >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
if [ -n "$DIFFS_URL" ]; then
|
|
echo "- [visual-diffs]($DIFFS_URL) — just the diffed screenshots (small, fast to review)" >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "**To update baselines:** merge to main (baselines auto-update on main runs) or run \`npx playwright test --update-snapshots\` locally." >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
# Also parse the JSON report for pass/fail counts
|
|
if [ -f playwright-report/results.json ]; then
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "### Test Results" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
node -e "
|
|
const r = require('./playwright-report/results.json');
|
|
const stats = r.stats || {};
|
|
console.log('| Status | Count |');
|
|
console.log('|--------|-------|');
|
|
console.log('| ✅ Passed | ' + (stats.expected || 0) + ' |');
|
|
console.log('| ❌ Failed | ' + (stats.unexpected || 0) + ' |');
|
|
console.log('| ⏭️ Skipped | ' + (stats.skipped || 0) + ' |');
|
|
console.log('| 🔄 Flaky | ' + (stats.flaky || 0) + ' |');
|
|
" >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true
|
|
fi
|