From 54ee9d5d11659c81694a0ca9a1ca80cc6c75a399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=86=A0=E8=BE=B0?= Date: Mon, 25 May 2026 14:52:42 +0800 Subject: [PATCH 1/3] test(e2e): expose core rpc failure diagnostics --- .github/workflows/e2e-reusable.yml | 7 ++--- app/test/core-rpc-node.test.ts | 17 ++++++++++++ app/test/e2e/helpers/core-rpc-node.ts | 38 +++++++++++++++++++++++++++ app/test/e2e/helpers/core-rpc.ts | 1 + app/test/e2e/specs/mega-flow.spec.ts | 26 ++++++++++-------- 5 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 app/test/core-rpc-node.test.ts diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index 26bfb0da20..b66196a741 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -159,6 +159,7 @@ jobs: with: name: e2e-failure-logs-${{ runner.os }}-${{ github.run_id }} path: | + ${{ runner.temp }}/openhuman-e2e-app-*.log /tmp/openhuman-e2e-app-*.log app/test/e2e/artifacts/ retention-days: 7 @@ -351,6 +352,7 @@ jobs: with: name: e2e-failure-logs-${{ runner.os }}-${{ matrix.shard.name }}-${{ github.run_id }} path: | + ${{ runner.temp }}/openhuman-e2e-app-*.log /tmp/openhuman-e2e-app-*.log app/test/e2e/artifacts/ retention-days: 7 @@ -734,6 +736,7 @@ jobs: with: name: e2e-failure-logs-${{ runner.os }}-${{ matrix.shard.name }}-${{ github.run_id }} path: | + ${{ runner.temp }}/openhuman-e2e-app-*.log /tmp/openhuman-e2e-app-*.log app/test/e2e/artifacts/ retention-days: 7 @@ -877,9 +880,7 @@ jobs: with: name: e2e-failure-logs-${{ runner.os }}-${{ matrix.shard.name }}-${{ github.run_id }} # e2e-run-session.sh writes its app log to `${RUNNER_TEMP:-${TMPDIR:-/tmp}}`. - # On Windows runners RUNNER_TEMP resolves to D:\a\_temp, not /tmp, so - # include the runner-temp pattern as well (Linux/macOS shards above - # use /tmp and don't need this). + # On Windows runners RUNNER_TEMP resolves to D:\a\_temp, not /tmp. path: | ${{ runner.temp }}/openhuman-e2e-app-*.log app/test/e2e/artifacts/ diff --git a/app/test/core-rpc-node.test.ts b/app/test/core-rpc-node.test.ts new file mode 100644 index 0000000000..05cc00bcc8 --- /dev/null +++ b/app/test/core-rpc-node.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { formatRpcCallFailure } from './e2e/helpers/core-rpc-node'; + +describe('formatRpcCallFailure', () => { + it('includes the RPC method, status, and error text', () => { + expect( + formatRpcCallFailure('openhuman.composio_list_triggers', { + ok: false, + httpStatus: 500, + error: 'Backend returned 500: trigger store unavailable', + }) + ).toContain( + 'openhuman.composio_list_triggers failed: httpStatus=500 error=Backend returned 500: trigger store unavailable' + ); + }); +}); diff --git a/app/test/e2e/helpers/core-rpc-node.ts b/app/test/e2e/helpers/core-rpc-node.ts index a672ea9f08..b5bb85ede0 100644 --- a/app/test/e2e/helpers/core-rpc-node.ts +++ b/app/test/e2e/helpers/core-rpc-node.ts @@ -20,6 +20,44 @@ let cachedRpcUrl: string | null = null; const E2E_TOKEN_FILENAME = 'openhuman-e2e-rpc-token'; +function truncate(value: string, maxLength = 500): string { + return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value; +} + +function safeJson(value: unknown): string { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +export function formatRpcCallFailure(method: string, result: RpcCallResult): string { + const parts = [`[core-rpc] ${method} failed:`]; + if (typeof result.httpStatus === 'number') { + parts.push(`httpStatus=${result.httpStatus}`); + } + if (result.error) { + parts.push(`error=${truncate(result.error)}`); + } + if (result.result !== undefined) { + parts.push(`result=${truncate(safeJson(result.result))}`); + } + if (parts.length === 1) { + parts.push(`payload=${truncate(safeJson(result))}`); + } + return parts.join(' '); +} + +export function expectRpcOk( + method: string, + result: RpcCallResult +): asserts result is RpcCallResult & { ok: true; result: T } { + if (!result.ok) { + throw new Error(formatRpcCallFailure(method, result)); + } +} + function readBearerToken(): string | null { const tokenPath = path.join(os.tmpdir(), E2E_TOKEN_FILENAME); try { diff --git a/app/test/e2e/helpers/core-rpc.ts b/app/test/e2e/helpers/core-rpc.ts index 4a809ec741..8c364525a1 100644 --- a/app/test/e2e/helpers/core-rpc.ts +++ b/app/test/e2e/helpers/core-rpc.ts @@ -17,6 +17,7 @@ import { callOpenhumanRpcNode } from './core-rpc-node'; import type { RpcCallResult } from './core-rpc-webview'; export type { RpcCallResult }; +export { expectRpcOk, formatRpcCallFailure } from './core-rpc-node'; export async function callOpenhumanRpc( method: string, diff --git a/app/test/e2e/specs/mega-flow.spec.ts b/app/test/e2e/specs/mega-flow.spec.ts index 00f806e6fb..216df0591b 100644 --- a/app/test/e2e/specs/mega-flow.spec.ts +++ b/app/test/e2e/specs/mega-flow.spec.ts @@ -31,7 +31,7 @@ import os from 'node:os'; import path from 'node:path'; import { waitForApp } from '../helpers/app-helpers'; -import { callOpenhumanRpc } from '../helpers/core-rpc'; +import { callOpenhumanRpc, expectRpcOk } from '../helpers/core-rpc'; import { triggerDeepLink } from '../helpers/deep-link-helpers'; import { hasAppChrome } from '../helpers/element-helpers'; import { @@ -247,8 +247,10 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { composioActiveTriggers: JSON.stringify([]), }); - const before = await callOpenhumanRpc('openhuman.composio_list_triggers', {}); - expect(before.ok).toBe(true); + const listTriggersMethod = 'openhuman.composio_list_triggers'; + const enableTriggerMethod = 'openhuman.composio_enable_trigger'; + const before = await callOpenhumanRpc(listTriggersMethod, {}); + expectRpcOk(listTriggersMethod, before); // list_triggers always emits a log line → RpcOutcome wraps in {result, logs}. // JSON-RPC result shape: { result: { triggers: [...] }, logs: [...] } // callResult.result = { result: { triggers: [...] }, logs: [...] } @@ -258,14 +260,14 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { expect(Array.isArray(beforeList)).toBe(true); expect(beforeList).toHaveLength(0); - const enable = await callOpenhumanRpc('openhuman.composio_enable_trigger', { + const enable = await callOpenhumanRpc(enableTriggerMethod, { connection_id: 'c1', slug: 'GMAIL_NEW_GMAIL_MESSAGE', }); - expect(enable.ok).toBe(true); + expectRpcOk(enableTriggerMethod, enable); - const after = await callOpenhumanRpc('openhuman.composio_list_triggers', {}); - expect(after.ok).toBe(true); + const after = await callOpenhumanRpc(listTriggersMethod, {}); + expectRpcOk(listTriggersMethod, after); const afterList = (after.result?.result?.triggers ?? after.result?.triggers ?? []) as unknown[]; expect(afterList.length).toBeGreaterThan(0); console.log(`${LOG} composio: enable mutated active list to`, afterList); @@ -573,11 +575,13 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { }); // Step 1 — enable trigger. - const enable = await callOpenhumanRpc('openhuman.composio_enable_trigger', { + const enableTriggerMethod = 'openhuman.composio_enable_trigger'; + const listTriggersMethod = 'openhuman.composio_list_triggers'; + const enable = await callOpenhumanRpc(enableTriggerMethod, { connection_id: 'c2', slug: 'GITHUB_PULL_REQUEST_EVENT', }); - expect(enable.ok).toBe(true); + expectRpcOk(enableTriggerMethod, enable); console.log(`${LOG} composio+webhook: trigger enabled`); // Step 2 — register an echo tunnel so the core has a tunnel ID to work with. @@ -617,8 +621,8 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { // Step 4 — verify the enabled trigger is still listed. // list_triggers always emits a log line → {result: {triggers:[...]}, logs:[...]} - const list = await callOpenhumanRpc('openhuman.composio_list_triggers', {}); - expect(list.ok).toBe(true); + const list = await callOpenhumanRpc(listTriggersMethod, {}); + expectRpcOk(listTriggersMethod, list); const triggers: unknown[] = list.result?.result?.triggers ?? list.result?.triggers ?? []; expect(triggers.length).toBeGreaterThan(0); console.log( From 12cb5a379c11e0fe17bf90afdb117319b4ecb1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=86=A0=E8=BE=B0?= Date: Mon, 25 May 2026 15:10:49 +0800 Subject: [PATCH 2/3] docs(e2e): document rpc diagnostics helpers --- app/test/e2e/helpers/core-rpc-node.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/test/e2e/helpers/core-rpc-node.ts b/app/test/e2e/helpers/core-rpc-node.ts index b5bb85ede0..9c8c8e5670 100644 --- a/app/test/e2e/helpers/core-rpc-node.ts +++ b/app/test/e2e/helpers/core-rpc-node.ts @@ -20,10 +20,12 @@ let cachedRpcUrl: string | null = null; const E2E_TOKEN_FILENAME = 'openhuman-e2e-rpc-token'; +/** Keep diagnostic payloads compact enough for CI assertion output. */ function truncate(value: string, maxLength = 500): string { return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value; } +/** Serialize arbitrary RPC payloads without throwing while formatting failures. */ function safeJson(value: unknown): string { try { return JSON.stringify(value); @@ -32,6 +34,7 @@ function safeJson(value: unknown): string { } } +/** Format failed RPC calls with the method name and any available transport/core error details. */ export function formatRpcCallFailure(method: string, result: RpcCallResult): string { const parts = [`[core-rpc] ${method} failed:`]; if (typeof result.httpStatus === 'number') { @@ -49,6 +52,7 @@ export function formatRpcCallFailure(method: string, result: RpcCallResult( method: string, result: RpcCallResult From dda88e1047f57eb767c0246fb6143db92756950d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=86=A0=E8=BE=B0?= Date: Mon, 25 May 2026 16:34:41 +0800 Subject: [PATCH 3/3] test(e2e): use file keyring backend in session runner --- app/scripts/e2e-run-session.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/scripts/e2e-run-session.sh b/app/scripts/e2e-run-session.sh index 0413a7d7c8..39af5239e8 100755 --- a/app/scripts/e2e-run-session.sh +++ b/app/scripts/e2e-run-session.sh @@ -74,6 +74,13 @@ else echo "[runner] Using OPENHUMAN_WORKSPACE from environment: $OPENHUMAN_WORKSPACE" fi +# Headless Linux CI does not always have a usable Secret Service/keychain. +# Keep E2E credentials under OPENHUMAN_WORKSPACE so auth state is deterministic +# and gets cleaned up with the rest of the test workspace. +: "${OPENHUMAN_KEYRING_BACKEND:=file}" +export OPENHUMAN_KEYRING_BACKEND +echo "[runner] Using OPENHUMAN_KEYRING_BACKEND: $OPENHUMAN_KEYRING_BACKEND" + # Place the CEF cache directory OUTSIDE the workspace. By default the Tauri # shell roots it under `$OPENHUMAN_WORKSPACE/users//cef`, but our # `mega-flow` spec calls `openhuman.config_reset_local_data` between