Skip to content
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
226a0f7
feat(e2e): move CI to Linux by default, keep macOS optional
CodeGhost21 Mar 31, 2026
30d246f
fix(e2e): fix login flow — config.toml injection, state cleanup, port…
CodeGhost21 Mar 31, 2026
fcea05f
fix(e2e): make onboarding walkthrough conditional in all flow specs
CodeGhost21 Mar 31, 2026
fc67275
fix(e2e): fix notion flow — auth assertion and navigation resilience
CodeGhost21 Mar 31, 2026
0f259db
fix(e2e): rewrite auth-access-control spec, add missing mock endpoints
CodeGhost21 Mar 31, 2026
da3c04b
fix(e2e): rewrite card and crypto payment flow specs
CodeGhost21 Mar 31, 2026
f92ec4c
Merge remote-tracking branch 'origin' into feat/e2e-linux-ci-default
CodeGhost21 Mar 31, 2026
9b981f3
fix(e2e): fix prettier formatting and login-flow syntax error
CodeGhost21 Mar 31, 2026
1b0414a
Merge branch 'main' into feat/e2e-linux-ci-default
senamakel Mar 31, 2026
c40af15
fix(e2e): format wdio.conf.ts with prettier
CodeGhost21 Mar 31, 2026
e7c80b0
fix(e2e): fix eslint errors — unused timeout param, unused eslint-dis…
CodeGhost21 Mar 31, 2026
f5d35c8
fix(e2e): add webkit2gtk-driver for tauri-driver on Linux CI
CodeGhost21 Mar 31, 2026
f8eb962
fix(e2e): add build artifact verification step in Linux CI
CodeGhost21 Apr 1, 2026
56b79bc
fix(local-ai): Ollama bootstrap failure UX and auto-recovery (#142)
senamakel Mar 31, 2026
f6a5326
fix(skills): persist OAuth credentials and fix skill auto-start lifec…
senamakel Mar 31, 2026
a01aa30
Update issue templates (#148)
senamakel Apr 1, 2026
82ed6a1
feat(agent): add self-learning subsystem with post-turn reflection (#…
senamakel Apr 1, 2026
8693ab9
feat(auth): Telegram bot registration flow — /auth/telegram endpoint …
senamakel Apr 1, 2026
3c2ff05
feat(webhooks): webhook tunnel routing for skills + remove legacy tun…
senamakel Apr 1, 2026
371e42b
feat(agent): architecture improvements — context guard, cost tracking…
senamakel Apr 1, 2026
f6aed0d
refactor(models): standardize to reasoning-v1, agentic-v1, coding-v1 …
senamakel Apr 1, 2026
9cf27b4
fix(skills): debug infrastructure + disconnect credential cleanup (#154)
senamakel Apr 1, 2026
61baf5d
feat(agent): multi-agent harness with 8 archetypes, DAG planning, and…
senamakel Apr 1, 2026
bcfe91f
chore(release): v0.50.0
github-actions[bot] Apr 1, 2026
2f0ce09
chore(release): disable Windows build notifications in release workflow
senamakel Apr 1, 2026
7aef315
chore(release): v0.50.1
github-actions[bot] Apr 1, 2026
7682a25
chore(release): v0.50.2
github-actions[bot] Apr 1, 2026
932aac3
chore(release): v0.50.3
github-actions[bot] Apr 1, 2026
dd64ea2
fix(e2e): address code review findings
CodeGhost21 Apr 1, 2026
6b460be
fix(e2e): add diagnostic logging for Linux CI session timeout
CodeGhost21 Apr 1, 2026
f4b57d3
fix(e2e): address code review findings
CodeGhost21 Apr 1, 2026
c91749f
fix(e2e): stage sidecar next to app binary for Linux CI
CodeGhost21 Apr 1, 2026
3b31175
fix(e2e): address code review findings
CodeGhost21 Apr 1, 2026
f5d6e9b
fix(e2e): add diagnostic logging for Linux CI session timeout
CodeGhost21 Apr 1, 2026
68a85ba
minor change
CodeGhost21 Apr 1, 2026
8b7711a
fix(e2e): make deep-link register_all non-fatal, add RUST_BACKTRACE
CodeGhost21 Apr 1, 2026
c420587
fix(e2e): JS click fallback for non-interactable elements on tauri-dr…
CodeGhost21 Apr 1, 2026
ee2333e
fix(e2e): scroll element into view before clicking on tauri-driver
CodeGhost21 Apr 1, 2026
c09a70b
fix(e2e): fix textExists and Settings navigation on Linux
CodeGhost21 Apr 1, 2026
9d4dbd1
Merge remote-tracking branch 'origin/main' into feat/e2e-linux-ci-def…
CodeGhost21 Apr 1, 2026
e74346c
fix: prettier formatting
CodeGhost21 Apr 1, 2026
8a198cf
fix(e2e): run Linux CI specs individually without fail-fast
CodeGhost21 Apr 1, 2026
aa34169
fix(e2e): split Linux CI into core and extended specs, skip macOS E2E
CodeGhost21 Apr 1, 2026
fb8ab00
fix(e2e): skip extended specs on Linux CI to avoid timeout
CodeGhost21 Apr 1, 2026
f4fc1a0
Merge remote-tracking branch 'origin/main' into feat/e2e-linux-ci-def…
CodeGhost21 Apr 1, 2026
3201ffb
fix(e2e): overhaul all E2E specs for Linux tauri-driver compatibility
CodeGhost21 Apr 1, 2026
c3a70e9
Merge branch 'main' into feat/e2e-linux-ci-default
CodeGhost21 Apr 1, 2026
ea23d2b
fix(e2e): harden specs with self-contained state, assertions, and dia…
CodeGhost21 Apr 1, 2026
8540ed4
fix(e2e): resolve typecheck failures and apply prettier formatting
CodeGhost21 Apr 1, 2026
6dfebf8
style: format wdio.conf.ts with prettier
CodeGhost21 Apr 1, 2026
8247cc7
fix(e2e): resolve eslint errors — remove unused eslint-disable and de…
CodeGhost21 Apr 1, 2026
5c26bb6
style: format login-flow.spec.ts with prettier
CodeGhost21 Apr 1, 2026
4ffe7fe
fix(e2e): fix CI failures in login-flow error path and onboarding-com…
CodeGhost21 Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ jobs:
${{ runner.os }}-e2e-cargo-

- name: Install tauri-driver
run: cargo install tauri-driver
run: cargo install tauri-driver --version 2.0.5

- name: Install JS dependencies
run: yarn install --frozen-lockfile
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/e2e-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ OS="$(uname)"
if [ "$OS" = "Linux" ]; then
# Linux: build debug binary only (no bundle needed for tauri-driver)
echo "Building for Linux (debug binary, no bundle)..."
npx tauri build -c "$TAURI_CONFIG_OVERRIDE" --debug
npx tauri build -c "$TAURI_CONFIG_OVERRIDE" --debug --no-bundle
else
# macOS: build .app bundle for Appium Mac2
echo "Building for macOS (.app bundle)..."
Expand Down
5 changes: 5 additions & 0 deletions app/scripts/e2e-run-all-flows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ run "test/e2e/specs/gmail-flow.spec.ts" "gmail"
run "test/e2e/specs/notion-flow.spec.ts" "notion"
run "test/e2e/specs/card-payment-flow.spec.ts" "card-payment"
run "test/e2e/specs/crypto-payment-flow.spec.ts" "crypto-payment"
run "test/e2e/specs/conversations-web-channel-flow.spec.ts" "conversations"
run "test/e2e/specs/local-model-runtime.spec.ts" "local-model"
run "test/e2e/specs/screen-intelligence.spec.ts" "screen-intelligence"
OPENHUMAN_SERVICE_MOCK=1 run "test/e2e/specs/service-connectivity-flow.spec.ts" "service-connectivity"
run "test/e2e/specs/skills-registry.spec.ts" "skills-registry"
run "test/e2e/specs/navigation.spec.ts" "navigation"
run "test/e2e/specs/smoke.spec.ts" "smoke"
run "test/e2e/specs/tauri-commands.spec.ts" "tauri-commands"
Expand Down
12 changes: 8 additions & 4 deletions app/test/e2e/helpers/element-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,15 @@ async function clickAtElement(el: ChainablePromiseElement): Promise<void> {
} catch {
// scrollIntoView may fail if element is detached
}
// Use JS click directly on tauri-driver — bypasses "element not interactable"
// and "element click intercepted" errors that WebDriver click triggers
// (WDIO retries WebDriver clicks 3 times internally before reaching catch,
// causing noisy WARN logs and slow failures).
try {
await el.click();
} catch {
// Fallback: use JS click which bypasses visibility checks
await browser.execute((e: HTMLElement) => e.click(), el as unknown as HTMLElement);
} catch {
// Last resort: try WebDriver click
await el.click();
}
return;
}
Expand Down Expand Up @@ -309,7 +313,7 @@ export async function clickToggle(_timeout: number = 15_000): Promise<void> {
for (const sel of selectors) {
const el = await browser.$(sel);
if (await el.isExisting()) {
await el.click();
await clickAtElement(el);
return;
}
}
Expand Down
319 changes: 319 additions & 0 deletions app/test/e2e/helpers/shared-flows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
// @ts-nocheck
/**
* Shared E2E flow helpers for Linux (tauri-driver).
*
* Extracted from individual spec files to avoid duplication.
* All navigation uses browser.execute() with window.location.hash
* because sidebar nav buttons are icon-only (aria-label, no text content).
*/
import { waitForAppReady, waitForAuthBootstrap } from './app-helpers';
import { triggerAuthDeepLink } from './deep-link-helpers';
import {
clickText,
dumpAccessibilityTree,
textExists,
waitForWebView,
waitForWindowVisible,
} from './element-helpers';

// ---------------------------------------------------------------------------
// Generic helpers
// ---------------------------------------------------------------------------

export async function waitForRequest(log, method, urlFragment, timeout = 15_000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const match = log().find(r => r.method === method && r.url.includes(urlFragment));
if (match) return match;
await browser.pause(500);
}
return undefined;
}

export async function waitForHomePage(timeout = 15_000) {
const candidates = [
'Test',
'Good morning',
'Good afternoon',
'Good evening',
'Message OpenHuman',
'Upgrade to Premium',
];
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
for (const text of candidates) {
if (await textExists(text)) return text;
}
await browser.pause(1_000);
}
return null;
}

export async function waitForTextToDisappear(text, timeout = 10_000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
if (!(await textExists(text))) return true;
await browser.pause(500);
}
return false;
}

/**
* Click the first matching text from a list of candidates.
*/
export async function clickFirstMatch(candidates, timeout = 5_000) {
for (const text of candidates) {
if (await textExists(text)) {
await clickText(text, timeout);
return text;
}
}
return null;
}

// ---------------------------------------------------------------------------
// Navigation helpers (JS hash-based — icon-only sidebar buttons)
// ---------------------------------------------------------------------------

export async function navigateViaHash(hash) {
try {
await browser.execute(h => {
window.location.hash = h;
}, hash);
await browser.pause(2_000);
const currentHash = await browser.execute(() => window.location.hash);
console.log(`[E2E] Navigated to ${hash} (current: ${currentHash})`);
} catch (err) {
console.log(`[E2E] Hash navigation to ${hash} failed:`, err);
}
}

export async function navigateToHome() {
await navigateViaHash('/home');
const homeText = await waitForHomePage(10_000);
if (!homeText) {
try {
await browser.execute(() => {
window.location.hash = '/home';
});
} catch {
/* ignore */
}
await browser.pause(2_000);
await waitForHomePage(10_000);
}
}

export async function navigateToSettings() {
await navigateViaHash('/settings');
}

export async function navigateToBilling() {
await navigateViaHash('/settings/billing');

const deadline = Date.now() + 15_000;
let hasBilling = false;
while (Date.now() < deadline) {
hasBilling =
(await textExists('Current Plan')) ||
(await textExists('FREE')) ||
(await textExists('Upgrade'));
Comment on lines +114 to +120
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

navigateToBilling() can still report success while the app is on Home.

textExists() is a contains match, so bare Upgrade also matches Home’s Upgrade to Premium. Both the initial loop and the final fallback check can therefore succeed without any billing-only marker on screen, and downstream specs can end up clicking the wrong CTA.

🧭 Tighten the billing check
-    hasBilling =
-      (await textExists('Current Plan')) ||
-      (await textExists('FREE')) ||
-      (await textExists('Upgrade'));
+    const currentHash = await browser.execute(() => window.location.hash);
+    hasBilling =
+      currentHash.startsWith('#/settings/billing') &&
+      ((await textExists('Current Plan')) ||
+        (await textExists('FREE')) ||
+        (await textExists('Manage')) ||
+        (await textExists('Billing & Usage')));
@@
-  const finalCheck =
-    (await textExists('Current Plan')) ||
-    (await textExists('FREE')) ||
-    (await textExists('Upgrade'));
+  const finalHash = await browser.execute(() => window.location.hash);
+  const finalCheck =
+    finalHash.startsWith('#/settings/billing') &&
+    ((await textExists('Current Plan')) ||
+      (await textExists('FREE')) ||
+      (await textExists('Manage')) ||
+      (await textExists('Billing & Usage')));
   if (!finalCheck) {
-    const finalHash = await browser.execute(() => window.location.hash);
     const tree = await dumpAccessibilityTree();

Also applies to: 155-160

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/test/e2e/helpers/shared-flows.ts` around lines 114 - 120, The
billing-detection in navigateToBilling() is too loose because
textExists('Upgrade') matches Home's "Upgrade to Premium"; update the checks
that call textExists(...) (both inside the polling loop and the final fallback)
to look for billing-specific markers only — e.g., replace the generic 'Upgrade'
check with a more specific string like 'Upgrade plan' or 'Manage billing' or use
an exact/whole-word match or a dedicated helper (e.g., textExistsExact or a
regex with word boundaries) so navigateToBilling() only returns success when
billing UI elements (Current Plan, FREE as billing badge, Manage billing/Upgrade
plan) are actually present. Ensure you modify the checks inside
navigateToBilling() and the final fallback check to use the same tightened
matching logic.

if (hasBilling) break;
await browser.pause(500);
}

if (hasBilling) {
console.log('[E2E] Billing page loaded');
return;
}

// Fallback
const currentHash = await browser.execute(() => window.location.hash);
console.log(`[E2E] Billing content not found. Current hash: ${currentHash}`);

await navigateViaHash('/settings');
await browser.pause(3_000);

const clicked = await browser.execute(() => {
const allText = document.querySelectorAll('*');
for (const el of allText) {
const text = el.textContent?.trim() || '';
if (
(text === 'Billing & Usage' || text === 'Billing') &&
el.closest('button, [role="button"], a, [class*="MenuItem"]')
) {
(el.closest('button, [role="button"], a, [class*="MenuItem"]') as HTMLElement).click();
return 'clicked';
}
}
window.location.hash = '/settings/billing';
return 'hash-fallback';
});
console.log(`[E2E] Billing fallback: ${clicked}`);
await browser.pause(3_000);

// Verify billing actually loaded after fallback
const finalCheck =
(await textExists('Current Plan')) ||
(await textExists('FREE')) ||
(await textExists('Upgrade'));
if (!finalCheck) {
const finalHash = await browser.execute(() => window.location.hash);
const tree = await dumpAccessibilityTree();
console.log(`[E2E] Billing verification failed after fallback. Hash: ${finalHash}`);
console.log(`[E2E] Accessibility tree:\n`, tree.slice(0, 4000));
throw new Error(
`navigateToBilling: billing markers not found after fallback (hash: ${finalHash})`
);
}
console.log('[E2E] Billing page loaded (after fallback)');
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export async function navigateToSkills() {
await navigateViaHash('/skills');
}

export async function navigateToIntelligence() {
await navigateViaHash('/intelligence');
}

export async function navigateToConversations() {
await navigateViaHash('/conversations');
}

// ---------------------------------------------------------------------------
// Onboarding walkthrough (Onboarding.tsx — 6 real steps)
// ---------------------------------------------------------------------------

/**
* Walk through the real onboarding steps:
* Step 0: WelcomeStep — "Continue"
* Step 1: LocalAIStep — "Use Local Models"
* Step 2: ScreenPermissions — "Continue Without Permission"
* Step 3: ToolsStep — "Continue"
* Step 4: SkillsStep — "Finish Setup" (fires onboarding-complete)
* Step 5: MnemonicStep — checkbox + "Finish Setup"
*/
export async function walkOnboarding(logPrefix = '[E2E]') {
// Detect onboarding overlay. The Onboarding.tsx parent renders a "Skip" defer
// button (top-right), and step 0 is WelcomeStep with "Continue".
const onboardingVisible =
(await textExists('Welcome')) ||
(await textExists('Skip')) ||
(await textExists('Use Local Models')) ||
(await textExists('Continue'));

if (!onboardingVisible) {
console.log(`${logPrefix} Onboarding overlay not visible — skipping`);
await browser.pause(3_000);
return;
}

// Step 0: WelcomeStep
if (await textExists('Welcome')) {
const clicked = await clickFirstMatch(['Continue'], 10_000);
if (clicked) console.log(`${logPrefix} WelcomeStep: clicked "${clicked}"`);
await browser.pause(2_000);
}

// Step 1: LocalAIStep — only has "Use Local Models" button now (no skip phase)
{
const clicked = await clickFirstMatch(['Use Local Models', 'Continue'], 10_000);
if (clicked) {
console.log(`${logPrefix} LocalAIStep: clicked "${clicked}"`);
await browser.pause(2_000);
}
}

// Step 2: ScreenPermissionsStep
{
const clicked = await clickFirstMatch(['Continue Without Permission', 'Continue'], 10_000);
if (clicked) {
console.log(`${logPrefix} ScreenPermissionsStep: clicked "${clicked}"`);
await browser.pause(2_000);
}
}

// Step 3: ToolsStep
{
if (await textExists('Enable Tools')) {
const clicked = await clickFirstMatch(['Continue'], 10_000);
if (clicked) {
console.log(`${logPrefix} ToolsStep: clicked "${clicked}"`);
await browser.pause(2_000);
}
}
}

// Step 4: SkillsStep
{
if (await textExists('Install Skills')) {
const clicked = await clickFirstMatch(['Finish Setup'], 10_000);
if (clicked) {
console.log(`${logPrefix} SkillsStep: clicked "${clicked}"`);
await browser.pause(3_000);
}
}
}

// Step 5: MnemonicStep
{
if (await textExists('Your Recovery Phrase')) {
console.log(`${logPrefix} MnemonicStep: visible`);
try {
await browser.execute(() => {
const checkbox = document.querySelector('input[type="checkbox"]') as HTMLInputElement;
if (checkbox && !checkbox.checked) checkbox.click();
});
} catch (err) {
console.log(`${logPrefix} MnemonicStep: checkbox failed:`, err);
}
await browser.pause(1_000);
const clicked = await clickFirstMatch(['Finish Setup'], 10_000);
if (clicked) {
console.log(`${logPrefix} MnemonicStep: clicked "${clicked}"`);
await browser.pause(3_000);
}
}
}
}

// ---------------------------------------------------------------------------
// Full login flow
// ---------------------------------------------------------------------------

/**
* @param token Deep link token string.
* @param logPrefix Prefix for console log lines.
* @param postLoginVerifier Optional async callback invoked after the Home page
* is confirmed. Receives `logPrefix` so it can log consistently. If the
* verifier throws, performFullLogin propagates the error — callers can use
* this to assert that auth side-effects (e.g. token consume, profile fetch)
* actually occurred rather than relying on UI alone.
*/
export async function performFullLogin(
token = 'e2e-test-token',
logPrefix = '[E2E]',
postLoginVerifier?: (logPrefix: string) => Promise<void>
) {
await triggerAuthDeepLink(token);
await waitForWindowVisible(25_000);
await waitForWebView(15_000);
await waitForAppReady(15_000);
await waitForAuthBootstrap(15_000);

await walkOnboarding(logPrefix);

const homeText = await waitForHomePage(15_000);
if (!homeText) {
const tree = await dumpAccessibilityTree();
console.log(`${logPrefix} Home page not reached after login. Tree:\n`, tree.slice(0, 4000));
throw new Error('Full login did not reach Home page');
}

if (postLoginVerifier) {
await postLoginVerifier(logPrefix);
}

console.log(`${logPrefix} Home page confirmed: found "${homeText}"`);
}
Loading
Loading