mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-13 13:49:15 +08:00
Compare commits
1 Commits
feat/opent
...
hermes-e2e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4312a9cc4e |
49
.github/workflows/e2e-cli-install.yml
vendored
Normal file
49
.github/workflows/e2e-cli-install.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: E2E CLI Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
e2e-tui-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: cd e2e && CI=true npm run test
|
||||
env:
|
||||
# Ensure tests don't accidentally call real APIs
|
||||
OPENROUTER_API_KEY: ""
|
||||
OPENAI_API_KEY: ""
|
||||
NOUS_API_KEY: ""
|
||||
|
||||
- name: Bundle TUI traces into self-contained replay HTML
|
||||
if: always()
|
||||
run: node e2e/scripts/bundle-replay-html.mjs
|
||||
|
||||
- name: Upload TUI replay viewer
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: tui-replay-viewer
|
||||
path: tui-replay-viewer/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload raw TUI test traces
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: tui-test-traces
|
||||
path: e2e/tui-traces/
|
||||
retention-days: 7
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,6 +19,8 @@ __pycache__/
|
||||
.notebooklm-playwright/
|
||||
.pip-cache/
|
||||
.uv-cache/
|
||||
.tui-test/
|
||||
tui-traces/
|
||||
compose.hermes.local.yml
|
||||
export*
|
||||
__pycache__/model_tools.cpython-310.pyc
|
||||
|
||||
14
e2e/package.json
Normal file
14
e2e/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "hermes-agent-e2e",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "npm exec @microsoft/tui-test -t",
|
||||
"replay": "npm exec @microsoft/tui-test show-trace"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/tui-test": "^0.0.4",
|
||||
"tui-replay": "^0.4.3"
|
||||
}
|
||||
}
|
||||
174
e2e/scripts/bundle-replay-html.mjs
Normal file
174
e2e/scripts/bundle-replay-html.mjs
Normal file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Bundle tui-replay traces into a single self-contained HTML file.
|
||||
*
|
||||
* Run from the repo root after e2e tests complete:
|
||||
* node e2e/scripts/bundle-replay-html.mjs
|
||||
*
|
||||
* Input: e2e/tui-traces/ (default @microsoft/tui-test output dir)
|
||||
* Output: tui-replay-viewer/replay.html (uploaded as a GHA artifact)
|
||||
*/
|
||||
import { createReplayDataSource } from 'tui-replay';
|
||||
import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
|
||||
import { resolve, join, dirname } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, '../..');
|
||||
|
||||
// tui-replay/dist/ — resolved via ESM so package exports are honoured
|
||||
const tuiReplayDist = dirname(fileURLToPath(import.meta.resolve('tui-replay')));
|
||||
|
||||
const tracesDir = resolve(repoRoot, 'e2e/tui-traces');
|
||||
const outputDir = resolve(repoRoot, 'tui-replay-viewer');
|
||||
const outputFile = join(outputDir, 'replay.html');
|
||||
|
||||
// ── exact strings to patch in client.js ────────────────────────────────────
|
||||
const SELECTORS_IMPORT =
|
||||
'import { annotationsForFrame, frameIndexAtTime, timelineItems } from "../preview/selectors.js";';
|
||||
|
||||
// Lines 166-172 of dist/viewer/client.js (0.4.x)
|
||||
const FETCH_ORIGINAL = `async function fetchPreviewModel() {
|
||||
const response = await fetch("/api/traces");
|
||||
if (!response.ok) {
|
||||
throw new Error(\`Unable to load traces: \${response.status}\`);
|
||||
}
|
||||
return (await response.json());
|
||||
}`;
|
||||
const FETCH_PATCHED = `async function fetchPreviewModel() {
|
||||
return __INLINE_MODEL__;
|
||||
}`;
|
||||
|
||||
// Lines 140-149 of dist/viewer/client.js (0.4.x)
|
||||
const CONNECT_ORIGINAL = `function connectLiveUpdates() {
|
||||
if (!("EventSource" in window)) {
|
||||
startPollingLiveUpdates();
|
||||
return;
|
||||
}
|
||||
const events = new EventSource("/api/events");
|
||||
events.addEventListener("model", (event) => {
|
||||
applyModelUpdate(JSON.parse(event.data));
|
||||
});
|
||||
}`;
|
||||
const CONNECT_PATCHED = `function connectLiveUpdates() {
|
||||
/* static mode: no live updates */
|
||||
}`;
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
// Gracefully skip when traces haven't been written yet (e.g. tests skipped)
|
||||
try {
|
||||
await access(tracesDir);
|
||||
} catch {
|
||||
console.log(`tui-traces dir not found at ${tracesDir} — skipping HTML bundle.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Loading traces from ${tracesDir} …`);
|
||||
const dataSource = createReplayDataSource({
|
||||
inputs: [tracesDir],
|
||||
projectRoot: repoRoot,
|
||||
});
|
||||
const model = await dataSource.load();
|
||||
|
||||
if (model.traces.length === 0) {
|
||||
console.log('No traces found — skipping HTML bundle.');
|
||||
process.exit(0);
|
||||
}
|
||||
console.log(`Found ${model.traces.length} trace(s).`);
|
||||
|
||||
// ── Load tui-replay dist assets ──────────────────────────────────────────
|
||||
// renderIndexHtml is internal (not in the public index.js export) so we
|
||||
// import it directly from the dist path.
|
||||
const { renderIndexHtml } = await import(
|
||||
pathToFileURL(join(tuiReplayDist, 'server/html.js')).href
|
||||
);
|
||||
|
||||
const [rawClientJs, rawSelectorsJs] = await Promise.all([
|
||||
readFile(join(tuiReplayDist, 'viewer/client.js'), 'utf8'),
|
||||
readFile(join(tuiReplayDist, 'preview/selectors.js'), 'utf8'),
|
||||
]);
|
||||
|
||||
// ── Patch client.js for static/embedded use ──────────────────────────────
|
||||
let clientJs = rawClientJs;
|
||||
|
||||
// 1. Remove the ES module import (selectors will be inlined above it)
|
||||
if (!clientJs.includes(SELECTORS_IMPORT)) {
|
||||
throw new Error(
|
||||
'Could not find selectors import in client.js — tui-replay may have updated. ' +
|
||||
'Please update the SELECTORS_IMPORT constant in bundle-replay-html.mjs.'
|
||||
);
|
||||
}
|
||||
clientJs = clientJs.replace(SELECTORS_IMPORT + '\n', '');
|
||||
|
||||
// 2. Replace the live fetch with a return of the inlined model
|
||||
if (!clientJs.includes(FETCH_ORIGINAL)) {
|
||||
throw new Error(
|
||||
'Could not find fetchPreviewModel body in client.js — tui-replay may have updated. ' +
|
||||
'Please update FETCH_ORIGINAL in bundle-replay-html.mjs.'
|
||||
);
|
||||
}
|
||||
clientJs = clientJs.replace(FETCH_ORIGINAL, FETCH_PATCHED);
|
||||
|
||||
// 3. Disable live-reload SSE/polling (no server in static mode)
|
||||
if (!clientJs.includes(CONNECT_ORIGINAL)) {
|
||||
throw new Error(
|
||||
'Could not find connectLiveUpdates body in client.js — tui-replay may have updated. ' +
|
||||
'Please update CONNECT_ORIGINAL in bundle-replay-html.mjs.'
|
||||
);
|
||||
}
|
||||
clientJs = clientJs.replace(CONNECT_ORIGINAL, CONNECT_PATCHED);
|
||||
|
||||
// Strip sourcemap comment (optional — keeps file clean in artifact viewer)
|
||||
clientJs = clientJs.replace(/\n\/\/#\s*sourceMappingURL=client\.js\.map\s*$/, '');
|
||||
|
||||
// ── Prepare selectors for inline use ─────────────────────────────────────
|
||||
// Remove `export` keyword so the functions are available in the same
|
||||
// module scope as client.js (they're no longer imported — they're just
|
||||
// declared above client.js in the same <script type="module"> block).
|
||||
const selectorsInline = rawSelectorsJs
|
||||
.replace(/^export function /gm, 'function ')
|
||||
.replace(/\n\/\/#\s*sourceMappingURL=selectors\.js\.map\s*$/, '');
|
||||
|
||||
// ── Embed model JSON ──────────────────────────────────────────────────────
|
||||
// JSON.stringify is safe inside a JS string but escape </script> sequences
|
||||
// just in case trace content contains them.
|
||||
const modelJsonString = JSON.stringify(model).replace(/<\/script>/gi, '<\\/script>');
|
||||
|
||||
// ── Assemble HTML ─────────────────────────────────────────────────────────
|
||||
const htmlTemplate = renderIndexHtml();
|
||||
|
||||
const SCRIPT_TAG = '<script type="module" src="/assets/client.js"></script>';
|
||||
if (!htmlTemplate.includes(SCRIPT_TAG)) {
|
||||
throw new Error(
|
||||
'Could not find the client script tag in the HTML template — ' +
|
||||
'tui-replay may have updated. Please update SCRIPT_TAG in bundle-replay-html.mjs.'
|
||||
);
|
||||
}
|
||||
|
||||
const inlinedHtml = htmlTemplate.replace(
|
||||
SCRIPT_TAG,
|
||||
`<script type="module">
|
||||
/* tui-replay selectors (inlined) */
|
||||
${selectorsInline}
|
||||
|
||||
/* trace model (embedded at bundle time) */
|
||||
const __INLINE_MODEL__ = JSON.parse(${JSON.stringify(modelJsonString)});
|
||||
|
||||
/* tui-replay client (patched for static mode) */
|
||||
${clientJs}
|
||||
</script>`
|
||||
);
|
||||
|
||||
// ── Write output ──────────────────────────────────────────────────────────
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
await writeFile(outputFile, inlinedHtml, 'utf8');
|
||||
|
||||
const sizeKb = (Buffer.byteLength(inlinedHtml, 'utf8') / 1024).toFixed(1);
|
||||
console.log(`✓ Wrote ${outputFile} (${sizeKb} KB, ${model.traces.length} trace(s))`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('bundle-replay-html failed:', err.message ?? err);
|
||||
process.exit(1);
|
||||
});
|
||||
30
e2e/test/cli.test.ts
Normal file
30
e2e/test/cli.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// import { test, expect } from "@microsoft/tui-test";
|
||||
// import {mkdtempSync, rmSync} from "fs"
|
||||
|
||||
// const CTRL_C = "\x03";
|
||||
|
||||
// test.describe("Hermes CLI basics", () => {
|
||||
// const HERMES_HOME = mkdtempSync("hermes-home")
|
||||
// test.use({
|
||||
// env: {HERMES_HOME},
|
||||
// })
|
||||
// test("hermes command is available and shows version", async ({ terminal }) => {
|
||||
// terminal.write("hermes --version\n");
|
||||
// // Wait for the version output to appear
|
||||
// await expect(terminal.getByText(/hermes/gi, { full: false })).toBeVisible({ timeout: 15000 });
|
||||
// });
|
||||
|
||||
// test("hermes setup wizard starts interactively", async ({ terminal }) => {
|
||||
// terminal.write("hermes setup\n");
|
||||
|
||||
// // Wait for the wizard to start (e.g., looking for "Configure Hermes Agent" or similar)
|
||||
// await expect(terminal.getByText(/configure|setup|wizard|api key/gi)).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// // Wait for the abort/exit message (KeyboardInterrupt is what python emits on ctrl+c)
|
||||
// await expect(terminal.getByText(/abort|cancel|exit|terminated|keyboardinterrupt/gi)).toBeVisible({ timeout: 5000 });
|
||||
// });
|
||||
|
||||
// test.afterAll(() => {
|
||||
// rmSync(HERMES_HOME, { force: true,recursive: true})
|
||||
// })
|
||||
// });
|
||||
24
e2e/test/install.test.ts
Normal file
24
e2e/test/install.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { test, expect, Shell } from "@microsoft/tui-test";
|
||||
import {mkdtempSync, rmSync} from "fs"
|
||||
|
||||
if(process.env.CI === "true") {
|
||||
test.describe("install hermes", () => {
|
||||
const HERMES_HOME = mkdtempSync("hermes-home")
|
||||
test.use({
|
||||
shell: Shell.Bash,
|
||||
env: {HERMES_HOME},
|
||||
})
|
||||
|
||||
test("hermes installer works", async ({ terminal }) => {
|
||||
// simulate curl | bash for installer script
|
||||
terminal.write("cat $GITHUB_WORKSPACE/scripts/install.sh | bash\n");
|
||||
// Wait for the version output to appear
|
||||
await expect(terminal.getByText(/asdfasdfasdf/gi, { full: false })).toBeVisible({ timeout: 150000 });
|
||||
});
|
||||
|
||||
test.afterAll(() => {
|
||||
rmSync(HERMES_HOME, { force: true,recursive: true})
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@ let
|
||||
|
||||
# Single npm deps fetch from the workspace root lockfile.
|
||||
# All workspace packages share this derivation.
|
||||
npmDepsHash = "sha256-jN6rD+vVhTCWz3lFZzlmFYXmcMRPTtYWy3XVSiDYbvM=";
|
||||
npmDepsHash = "sha256-xs98fk+09BWHqq9fsjtGWD23BOVRzfFmRwnOVvv6lv8=";
|
||||
|
||||
npmDeps = pkgs.fetchNpmDeps {
|
||||
inherit src;
|
||||
|
||||
1792
package-lock.json
generated
1792
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,8 @@
|
||||
"apps/*",
|
||||
"ui-tui",
|
||||
"ui-tui/packages/*",
|
||||
"web"
|
||||
"web",
|
||||
"e2e"
|
||||
],
|
||||
"scripts": {
|
||||
"postinstall": "echo '✅ Browser tools ready. Run: python run_agent.py --help'",
|
||||
|
||||
1816
tui-replay-viewer/replay.html
Normal file
1816
tui-replay-viewer/replay.html
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user