Files
openhuman/app/test/e2e/specs/skill-execution-flow.spec.ts
T
Mega MindandGitHub b7b3d62c58 test(e2e): skill execution flow (core RPC + Skills UI) (#283)
* test(e2e): skill execution flow (core RPC + Skills UI)

- Add skill-execution-flow.spec: core.ping, skills start/list_tools/call_tool/stop
  via shared RPC helper, seeded QuickJS echo skill (same tree as Rust json_rpc_e2e).
- Add core-rpc helpers: WebView invoke on tauri-driver, Node fetch + port probe on
  Appium Mac2 (no WKWebView execute).
- Extend navigateViaHash with Mac2 sidebar label fallbacks for /skills, /home, etc.
- Wire yarn test:e2e:skill-execution and e2e-run-all-flows.

Refs: #68
Made-with: Cursor

* fix(e2e): address CodeRabbit review for skill execution PR

- core-rpc-node: honor OPENHUMAN_CORE_HOST and OPENHUMAN_CORE_PORT before port scan
- shared-flows: Mac2 navigateViaHash for /settings/billing; throw on unmapped hash;
  navigateToBilling fallback without WebView execute on Mac2
- skill-e2e-runtime: javascript manifest + removeSeededEchoSkill teardown
- skill-execution-flow: teardown seeded skill; document RPC shapes vs json_rpc_e2e;
  skip placeholder for future agent chat tool_calls

Made-with: Cursor
2026-04-03 12:00:01 +05:30

142 lines
5.2 KiB
TypeScript

// @ts-nocheck
/**
* End-to-end: core JSON-RPC skill runtime (UI WebView → HTTP POST to sidecar) plus Skills UI smoke.
* Mirrors the Rust integration test `json_rpc_skills_runtime_start_tools_call_stop` (tests/json_rpc_e2e.rs).
*
* JSON-RPC `result` shapes match that test: `skills_start` → `SkillSnapshot` (e.g. `status`, `skill_id`);
* `skills_call_tool` → `ToolResult` (`content[]`); `skills_stop` → `{ success, skill_id }`. Not wrapped in `{ skill }` / `{ result }`.
*
* Issue #68 also asks for model→agent→tool→conversation; that path is environment- and LLM-dependent.
* This spec validates the **skill runtime + RPC + Skills shell** deterministically; full chat tool-calls belong
* in agent integration tests when the mock/backend can return structured tool_calls.
*/
import { waitForApp, waitForAppReady } from '../helpers/app-helpers';
import { callOpenhumanRpc } from '../helpers/core-rpc';
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
import {
dumpAccessibilityTree,
textExists,
waitForWebView,
waitForWindowVisible,
} from '../helpers/element-helpers';
import { supportsExecuteScript } from '../helpers/platform';
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
import {
E2E_RUNTIME_SKILL_ID,
removeSeededEchoSkill,
seedMinimalEchoSkill,
} from '../helpers/skill-e2e-runtime';
import { clearRequestLog, getRequestLog, startMockServer, stopMockServer } from '../mock-server';
function stepLog(message: string, context?: unknown): void {
const stamp = new Date().toISOString();
if (context === undefined) {
console.log(`[SkillExecutionE2E][${stamp}] ${message}`);
return;
}
console.log(`[SkillExecutionE2E][${stamp}] ${message}`, JSON.stringify(context, null, 2));
}
describe('Skill execution (UI + core RPC)', () => {
before(async () => {
stepLog('Seeding minimal echo skill on disk');
await seedMinimalEchoSkill();
await startMockServer();
await waitForApp();
clearRequestLog();
});
after(async () => {
await stopMockServer();
await removeSeededEchoSkill();
});
it('authenticates and reaches a logged-in shell', async () => {
await triggerAuthDeepLinkBypass('e2e-skill-execution-token');
await waitForWindowVisible(25_000);
await waitForWebView(15_000);
await waitForAppReady(15_000);
await completeOnboardingIfVisible('[SkillExecutionE2E]');
const atHome =
(await textExists('Message OpenHuman')) ||
(await textExists('Good morning')) ||
(await textExists('Upgrade to Premium'));
expect(atHome).toBe(true);
});
it('core.ping responds over the same JSON-RPC URL as the UI', async () => {
const ping = await callOpenhumanRpc('core.ping', {});
if (!ping.ok) {
stepLog('core.ping failed', ping);
}
expect(ping.ok).toBe(true);
});
it('runs start → list_tools → call_tool → stop for the seeded echo skill', async () => {
const start = await callOpenhumanRpc('openhuman.skills_start', {
skill_id: E2E_RUNTIME_SKILL_ID,
});
if (!start.ok) {
stepLog('skills_start failed', start);
stepLog('Request log (mock API):', getRequestLog());
}
expect(start.ok).toBe(true);
const status = start.result?.status;
expect(status === 'running' || status === 'initializing').toBe(true);
await browser.pause(800);
const tools = await callOpenhumanRpc('openhuman.skills_list_tools', {
skill_id: E2E_RUNTIME_SKILL_ID,
});
expect(tools.ok).toBe(true);
const toolNames = (tools.result?.tools || []).map((t: { name?: string }) => t.name);
expect(toolNames.includes('echo')).toBe(true);
const call = await callOpenhumanRpc('openhuman.skills_call_tool', {
skill_id: E2E_RUNTIME_SKILL_ID,
tool_name: 'echo',
arguments: { message: 'hello from e2e skill execution' },
});
expect(call.ok).toBe(true);
const content = call.result?.content || [];
const echoed = content.some(
(c: { text?: string }) =>
typeof c?.text === 'string' && c.text.includes('hello from e2e skill execution')
);
expect(echoed).toBe(true);
const stop = await callOpenhumanRpc('openhuman.skills_stop', {
skill_id: E2E_RUNTIME_SKILL_ID,
});
expect(stop.ok).toBe(true);
expect(stop.result?.success === true).toBe(true);
});
it('Skills page loads (UI surface for installed tools)', async () => {
await navigateToSkills();
await browser.pause(2_000);
if (supportsExecuteScript()) {
const hash = await browser.execute(() => window.location.hash);
expect(String(hash)).toContain('/skills');
}
const visible =
(await textExists('Skills')) ||
(await textExists('Install')) ||
(await textExists('Available')) ||
(await textExists('Telegram')) ||
(await textExists('Notion'));
if (!visible) {
stepLog('Skills markers missing');
await dumpAccessibilityTree();
stepLog('Request log:', getRequestLog());
}
expect(visible).toBe(true);
});
it.skip('(future) agent chat issues model tool_calls to echo — needs LLM + mock tool_calls', async () => {
// Tracked under #68: drive Conversations with a prompt that forces tool use and assert echo in thread.
});
});