Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6cabc89
docs(testing): add test coverage matrix for product feature list (#773)
oxoxDev Apr 27, 2026
f4e6fa8
docs(testing): add layered testing strategy + decision tree (#773)
oxoxDev Apr 27, 2026
6bca2c8
test(memory): add Rust integration test for store/recall/clear roundt…
oxoxDev Apr 27, 2026
820ba24
test(intelligence): add Vitest for actionable item utils (#773)
oxoxDev Apr 27, 2026
2b1c22f
test(autocomplete): add Vitest for useAutocompleteSkillStatus hook (#…
oxoxDev Apr 27, 2026
046a311
test(e2e): add WhatsApp account integration smoke spec (#773)
oxoxDev Apr 27, 2026
591dd30
test(e2e): add Slack account integration smoke spec (#773)
oxoxDev Apr 27, 2026
11fc7ef
test(e2e): add memory subsystem round-trip spec via JSON-RPC (#773)
oxoxDev Apr 27, 2026
473c534
test(e2e): add Insights dashboard smoke spec (#773)
oxoxDev Apr 27, 2026
c067695
test(e2e): add autocomplete settings smoke spec (#773)
oxoxDev Apr 27, 2026
110598b
chore(deps): align app/src-tauri/Cargo.lock to v0.53.3 after upstream…
oxoxDev Apr 27, 2026
3755ccd
test(intelligence): pin wall clock in utils.test to fix day-boundary …
oxoxDev Apr 27, 2026
89e01ac
test(e2e): isolate WhatsApp flow tests + assert account registration …
oxoxDev Apr 27, 2026
c00e45d
test(e2e): isolate Slack flow tests + assert account registration via…
oxoxDev Apr 27, 2026
edf76b2
test(e2e): seed canary inside clear-namespace test so it cannot pass …
oxoxDev Apr 27, 2026
72667b5
test(e2e): tighten insights-dashboard selectors to real UI ids (#773)
oxoxDev Apr 27, 2026
8b79cc9
test(e2e): narrow autocomplete spec scope + isolate the CTA test (#773)
oxoxDev Apr 27, 2026
ca51764
docs(testing): downgrade autocomplete + analyze-trigger rows + lint f…
oxoxDev Apr 27, 2026
b662ccc
test(memory): wrap env mutations in unsafe blocks per Rust 1.83 (#773)
oxoxDev Apr 28, 2026
c6a76d4
docs(testing): annotate RELEASE-MANUAL-SMOKE.md as not-yet-created (#…
oxoxDev Apr 28, 2026
29da38c
feat(store): expose Redux store on window for E2E backing-state check…
oxoxDev Apr 28, 2026
6f85b2e
test(e2e): hoist openAddAccountModal to shared-flows + harden Redux a…
oxoxDev Apr 28, 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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

183 changes: 183 additions & 0 deletions app/src/components/intelligence/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

import type { ActionableItem } from '../../../types/intelligence';
import { filterItems, getItemStats, groupItemsByTime } from '../utils';

// Pin the wall clock so day-boundary buckets are stable across the day and on CI.
const FIXED_NOW = new Date('2026-04-27T12:00:00.000Z');

beforeAll(() => {
vi.useFakeTimers();
vi.setSystemTime(FIXED_NOW);
});

afterAll(() => {
vi.useRealTimers();
});

function makeItem(
partial: Partial<ActionableItem> & { id: string; createdAt: Date }
): ActionableItem {
return {
title: partial.title ?? 'Untitled',
description: partial.description,
source: partial.source ?? 'system',
priority: partial.priority ?? 'normal',
status: partial.status ?? 'active',
updatedAt: partial.updatedAt ?? partial.createdAt,
actionable: partial.actionable ?? false,
sourceLabel: partial.sourceLabel,
expiresAt: partial.expiresAt,
...partial,
} as ActionableItem;
}

const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;

function daysAgo(n: number): Date {
return new Date(Date.now() - n * DAY);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

describe('intelligence/utils — actionable item helpers (11.1.2)', () => {
describe('groupItemsByTime', () => {
it('buckets items into Today / Yesterday / This Week / Older', () => {
const items = [
makeItem({ id: 'today', createdAt: new Date() }),
makeItem({ id: 'yest', createdAt: daysAgo(1) }),
makeItem({ id: 'wk', createdAt: daysAgo(3) }),
makeItem({ id: 'old', createdAt: daysAgo(30) }),
];

const groups = groupItemsByTime(items);
const labels = groups.map(g => g.label);
expect(labels).toEqual(['Today', 'Yesterday', 'This Week', 'Older']);

const find = (label: string) => groups.find(g => g.label === label);
expect(find('Today')?.items.map(i => i.id)).toEqual(['today']);
expect(find('Yesterday')?.items.map(i => i.id)).toEqual(['yest']);
expect(find('This Week')?.items.map(i => i.id)).toEqual(['wk']);
expect(find('Older')?.items.map(i => i.id)).toEqual(['old']);
});

it('omits empty buckets and orders within a bucket by priority then recency', () => {
const items = [
makeItem({ id: 'a', createdAt: new Date(), priority: 'normal' }),
makeItem({ id: 'b', createdAt: new Date(Date.now() - 2 * HOUR), priority: 'critical' }),
makeItem({ id: 'c', createdAt: new Date(Date.now() - HOUR), priority: 'critical' }),
];

const groups = groupItemsByTime(items);
expect(groups).toHaveLength(1);
const today = groups[0];
expect(today.label).toBe('Today');
// Critical first; within critical, newer first; normal last.
expect(today.items.map(i => i.id)).toEqual(['c', 'b', 'a']);
expect(today.count).toBe(3);
});

it('handles an empty input as an empty group list', () => {
expect(groupItemsByTime([])).toEqual([]);
});
});

describe('filterItems', () => {
const items = [
makeItem({
id: '1',
createdAt: new Date(),
title: 'Reply to Alice',
source: 'email',
priority: 'critical',
status: 'active',
sourceLabel: 'Gmail',
}),
makeItem({
id: '2',
createdAt: new Date(),
title: 'Standup',
source: 'calendar',
priority: 'normal',
status: 'active',
sourceLabel: 'Calendar',
}),
makeItem({
id: '3',
createdAt: new Date(),
title: 'Reply to Bob',
source: 'email',
priority: 'important',
status: 'completed',
sourceLabel: 'Gmail',
description: 'Follow-up on the canary deployment',
}),
];

it('filters by source', () => {
const out = filterItems(items, { source: 'email' });
expect(out.map(i => i.id)).toEqual(['1', '3']);
});

it('filters by priority', () => {
const out = filterItems(items, { priority: 'critical' });
expect(out.map(i => i.id)).toEqual(['1']);
});

it('filters by status', () => {
const out = filterItems(items, { status: 'completed' });
expect(out.map(i => i.id)).toEqual(['3']);
});

it('filters by searchTerm across title, description, and sourceLabel', () => {
expect(filterItems(items, { searchTerm: 'reply' }).map(i => i.id)).toEqual(['1', '3']);
expect(filterItems(items, { searchTerm: 'canary' }).map(i => i.id)).toEqual(['3']);
expect(filterItems(items, { searchTerm: 'gmail' }).map(i => i.id)).toEqual(['1', '3']);
});
Comment on lines +131 to +135

@coderabbitai coderabbitai Bot Apr 27, 2026 •

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

searchTerm expectation includes sourceLabel, but implementation currently does not.

Line 122 expects 'gmail' to match via sourceLabel, but app/src/components/intelligence/utils.ts (filter implementation) only searches title and description. This will fail unless filterItems is updated.

If `sourceLabel` matching is intended, patch `filterItems`
--- a/app/src/components/intelligence/utils.ts
+++ b/app/src/components/intelligence/utils.ts
@@
   if (options.searchTerm) {
     const term = options.searchTerm.toLowerCase();
     filtered = filtered.filter(item =>
       item.title.toLowerCase().includes(term) ||
-      (item.description && item.description.toLowerCase().includes(term))
+      (item.description && item.description.toLowerCase().includes(term)) ||
+      (item.sourceLabel && item.sourceLabel.toLowerCase().includes(term))
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/intelligence/__tests__/utils.test.ts` around lines 119 -
123, The test expects filterItems to match searchTerm against sourceLabel as
well, but the current implementation in app/src/components/intelligence/utils.ts
only checks title and description; update the filterItems function so the
searchTerm is compared case-insensitively against item.sourceLabel in the same
way it checks title and description (e.g., normalize to lower/upper and include
sourceLabel in the OR conditions or combined searchable string used for
matching) so that searches like 'gmail' will return items whose sourceLabel
contains that term.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

False positive — filterItems in app/src/components/intelligence/utils.ts does include sourceLabel in its searchTerm filter:

filtered = filtered.filter(
  item =>
    item.title.toLowerCase().includes(term) ||
    item.description?.toLowerCase().includes(term) ||
    item.sourceLabel?.toLowerCase().includes(term)
);

The Vitest case is correct as-is.

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "sourceLabel" app/src/components/intelligence/utils.ts -C 5

Length of output: 333


@oxoxDev, you're correct — confirmed. Line 100 of app/src/components/intelligence/utils.ts already includes:

item.sourceLabel?.toLowerCase().includes(term)

My comment was a false positive. The test and the implementation are consistent. Apologies for the noise!


✏️ Learnings added
Learnt from: oxoxDev
Repo: tinyhumansai/openhuman PR: 980
File: app/src/components/intelligence/__tests__/utils.test.ts:131-135
Timestamp: 2026-04-27T12:17:54.782Z
Learning: In `app/src/components/intelligence/utils.ts`, the `filterItems` function's `searchTerm` filter already checks `item.title`, `item.description`, **and** `item.sourceLabel` (all case-insensitively via `.toLowerCase().includes(term)`). Do not flag `sourceLabel` as missing from the filter in future reviews of this file.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: tinyhumansai/openhuman PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-26T06:17:19.282Z
Learning: Applies to app/src/**/*.test.{ts,tsx} : Co-locate unit tests as `*.test.ts` / `*.test.tsx` under `app/src/**`. Prefer behavior over implementation. Use helpers in `app/src/test/`. No real network, no time flakes.

Learnt from: graycyrus
Repo: tinyhumansai/openhuman PR: 888
File: app/src/pages/Conversations.tsx:785-796
Timestamp: 2026-04-24T15:11:32.371Z
Learning: In `app/src/components/intelligence/ConfirmationModal.tsx` (used by `app/src/pages/Conversations.tsx` PR `#888`), unit test coverage for the `ConfirmationModal` component (render-null guard, confirm path, cancel/backdrop path, destructive styling) was intentionally deferred to a follow-up. The rationale: `Conversations.tsx` has deep Redux/Tauri dependencies not covered by the current test store, and the confirmation flow is fully delegated to `ConfirmationModal`, making it the correct isolated test target. Tests should be placed at `app/src/components/intelligence/ConfirmationModal.test.tsx` using Vitest + `testing-library/react`.

Learnt from: graycyrus
Repo: tinyhumansai/openhuman PR: 772
File: app/src/pages/onboarding/steps/WelcomeStep.tsx:86-86
Timestamp: 2026-04-22T08:39:02.109Z
Learning: In `app/src/pages/onboarding/steps/WelcomeStep.tsx`, the `OnboardingNextButton` label on `WelcomeStep` has always been `"Let's Start"` on `main` (not `"Continue"`). This is a pre-existing state and should not be flagged as a regression in any future PR. The E2E helper `clickFirstMatch(['Continue'])` returns `null` gracefully when no match is found, so the label mismatch is non-blocking for the test flow.

Learnt from: CR
Repo: tinyhumansai/openhuman PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T17:30:20.577Z
Learning: Applies to app/test/e2e/specs/*.spec.ts : Assert both UI outcomes and backend/mock effects in E2E tests when relevant; add failure diagnostics (request logs, `dumpAccessibilityTree()`) for faster debugging

Learnt from: CR
Repo: tinyhumansai/openhuman PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T17:30:20.577Z
Learning: Ship enough unit tests and coverage for behavior before building additional features on top of it — treat untested code as incomplete and do not accumulate depth on a shaky base


it('treats "all" as a no-op for source/priority/status', () => {
const out = filterItems(items, { source: 'all', priority: 'all', status: 'all' });
expect(out.map(i => i.id)).toEqual(['1', '2', '3']);
});

it('returns no items when searchTerm matches nothing (failure path)', () => {
const out = filterItems(items, { searchTerm: 'definitely-not-present' });
expect(out).toEqual([]);
});
});

describe('getItemStats', () => {
it('counts totals, priorities, and sources, and flags new + expiringSoon', () => {
const items = [
makeItem({
id: 'fresh',
createdAt: new Date(Date.now() - 60 * 1000), // 1 minute ago
priority: 'critical',
source: 'email',
}),
makeItem({
id: 'old',
createdAt: new Date(Date.now() - 10 * DAY),
priority: 'normal',
source: 'calendar',
}),
makeItem({
id: 'expSoon',
createdAt: new Date(Date.now() - 2 * HOUR),
priority: 'important',
source: 'email',
expiresAt: new Date(Date.now() + 6 * HOUR),
}),
];

const stats = getItemStats(items);
expect(stats.total).toBe(3);
expect(stats.byPriority.critical).toBe(1);
expect(stats.byPriority.important).toBe(1);
expect(stats.byPriority.normal).toBe(1);
expect(stats.bySource.email).toBe(2);
expect(stats.bySource.calendar).toBe(1);
expect(stats.newItems).toBe(1);
expect(stats.expiringSoon).toBe(1);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

import { useCoreState } from '../../../providers/CoreStateProvider';
import { useAutocompleteSkillStatus } from '../useAutocompleteSkillStatus';

vi.mock('../../../providers/CoreStateProvider', () => ({ useCoreState: vi.fn() }));

type AutocompleteRuntime = {
platform_supported: boolean;
running: boolean;
enabled: boolean;
last_error?: string | null;
};

function mockSnapshot(autocomplete: AutocompleteRuntime | null): void {
vi.mocked(useCoreState).mockReturnValue({ snapshot: { runtime: { autocomplete } } } as ReturnType<
typeof useCoreState
>);
}

describe('useAutocompleteSkillStatus (5.2 — autocomplete settings status)', () => {
it('returns offline + Enable CTA when no runtime status is available yet', () => {
mockSnapshot(null);
const { result } = renderHook(() => useAutocompleteSkillStatus());
expect(result.current.connectionStatus).toBe('offline');
expect(result.current.statusLabel).toBe('Offline');
expect(result.current.ctaLabel).toBe('Enable');
expect(result.current.platformUnsupported).toBe(false);
});

it('returns Unsupported when the platform reports the runtime is unsupported', () => {
mockSnapshot({ platform_supported: false, running: false, enabled: false });
const { result } = renderHook(() => useAutocompleteSkillStatus());
expect(result.current.connectionStatus).toBe('offline');
expect(result.current.statusLabel).toBe('Unsupported');
expect(result.current.ctaLabel).toBe('Details');
expect(result.current.platformUnsupported).toBe(true);
});

it('returns Active + Manage CTA when the runtime is running (overrides stale errors)', () => {
mockSnapshot({
platform_supported: true,
running: true,
enabled: true,
last_error: 'stale: should not surface',
});
const { result } = renderHook(() => useAutocompleteSkillStatus());
expect(result.current.connectionStatus).toBe('connected');
expect(result.current.statusLabel).toBe('Active');
expect(result.current.ctaLabel).toBe('Manage');
});

it('returns Error + Retry CTA when not running and an error is present', () => {
mockSnapshot({
platform_supported: true,
running: false,
enabled: true,
last_error: 'permission denied',
});
const { result } = renderHook(() => useAutocompleteSkillStatus());
expect(result.current.connectionStatus).toBe('error');
expect(result.current.statusLabel).toBe('Error');
expect(result.current.ctaLabel).toBe('Retry');
expect(result.current.ctaVariant).toBe('amber');
});

it('returns Enabled (disconnected) + Manage when enabled but not running and no error', () => {
mockSnapshot({ platform_supported: true, running: false, enabled: true, last_error: null });
const { result } = renderHook(() => useAutocompleteSkillStatus());
expect(result.current.connectionStatus).toBe('disconnected');
expect(result.current.statusLabel).toBe('Enabled');
expect(result.current.ctaLabel).toBe('Manage');
});

it('returns Disabled + Enable CTA when not enabled and not running', () => {
mockSnapshot({ platform_supported: true, running: false, enabled: false, last_error: null });
const { result } = renderHook(() => useAutocompleteSkillStatus());
expect(result.current.connectionStatus).toBe('offline');
expect(result.current.statusLabel).toBe('Disabled');
expect(result.current.ctaLabel).toBe('Enable');
expect(result.current.ctaVariant).toBe('sage');
});
});
8 changes: 8 additions & 0 deletions app/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,13 @@ export const store = configureStore({

export const persistor = persistStore(store);

// Expose the store on `window` so WDIO E2E specs can read Redux state directly
// to assert backing-state changes (see app/test/e2e/specs/*.spec.ts). The store
// holds no secrets that aren't already in the renderer's memory; this only
// surfaces the existing handle under a stable, namespaced key.
if (typeof window !== 'undefined') {
(window as unknown as { __OPENHUMAN_STORE__?: typeof store }).__OPENHUMAN_STORE__ = store;
}
Comment on lines +75 to +81

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

Restrict global store exposure to test/dev builds.

Line 79 currently exposes window.__OPENHUMAN_STORE__ in all renderer runtimes, not just E2E contexts. That widens read/write access to full Redux state in packaged app sessions. Please gate this behind a non-production config flag (at minimum IS_DEV, or a dedicated E2E flag from app/src/utils/config) and define it as non-writable to reduce accidental mutation.

Suggested hardening
-// Expose the store on `window` so WDIO E2E specs can read Redux state directly
+// Expose the store on `window` only in non-production contexts so WDIO E2E
+// specs can read Redux state directly.
 // to assert backing-state changes (see app/test/e2e/specs/*.spec.ts). The store
 // holds no secrets that aren't already in the renderer's memory; this only
 // surfaces the existing handle under a stable, namespaced key.
-if (typeof window !== 'undefined') {
-  (window as unknown as { __OPENHUMAN_STORE__?: typeof store }).__OPENHUMAN_STORE__ = store;
+if (typeof window !== 'undefined' && IS_DEV) {
+  Object.defineProperty(window as Window & { __OPENHUMAN_STORE__?: typeof store }, '__OPENHUMAN_STORE__', {
+    value: store,
+    writable: false,
+    enumerable: false,
+    configurable: true,
+  });
 }

As per coding guidelines: “Frontend config is centralized in app/src/utils/config.ts. Read VITE_* there and re-export — never import import.meta.env directly elsewhere.”

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

In `@app/src/store/index.ts` around lines 75 - 81, Guard the global exposure of
window.__OPENHUMAN_STORE__ so it only occurs in non-production/testing builds:
import the centralized flag (e.g. IS_DEV or IS_E2E) from the app config module,
wrap the existing assignment in an if (IS_DEV || IS_E2E) check, and instead of
direct assignment use Object.defineProperty(window, "__OPENHUMAN_STORE__", {
value: store, writable: false, configurable: false, enumerable: false }) to make
it read-only and non-enumerable; keep the typeof window !== 'undefined' check
and the reference to the Redux store symbol named store unchanged.


export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
30 changes: 30 additions & 0 deletions app/test/e2e/helpers/shared-flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,41 @@ import {
clickText,
dumpAccessibilityTree,
textExists,
waitForText,
waitForWebView,
waitForWindowVisible,
} from './element-helpers';
import { supportsExecuteScript } from './platform';

// ---------------------------------------------------------------------------
// Accounts page helpers
// ---------------------------------------------------------------------------

/**
* Open the "Add Account" modal on /accounts.
*
* The "Add app" affordance is a button whose only labelled descendants are an
* SVG plus a tooltip span with `pointer-events: none`. None of the shared
* `clickButton`/`clickText` helpers can target it cleanly because the
* accessible name lives only on `aria-label`, so this helper reaches for the
* explicit selector. Tracking a follow-up `clickByAriaLabel` helper.
*/
export async function openAddAccountModal(): Promise<void> {
const opened = await browser.execute(() => {
const buttons = Array.from(document.querySelectorAll<HTMLButtonElement>('button'));
const addBtn = buttons.find(b => b.getAttribute('aria-label') === 'Add app');
if (addBtn) {
addBtn.click();
return true;
}
return false;
});
if (!opened) {
throw new Error('Could not locate Add app button on /accounts');
}
await waitForText('Add account', 5_000);
}

// ---------------------------------------------------------------------------
// Generic helpers
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading