Skip to content

Commit d153488

Browse files
committed
fix(e2e): harden assertions, fix Mac2 navigation, update Skills page selectors
E2E spec fixes across all flow specs: - Replace tautological `res.ok || Boolean(res.error)` assertions with explicit success/failure checks and diagnostic logging in discord-flow, gmail-flow, notion-flow, macos-distribution, automation-scheduling, chat-interface-flow, system-resource-access, and login-flow specs - Fix channel_web_chat payload shape: use {client_id, thread_id, message} instead of wrong {input, channel, target} keys - Update Skills page selectors for new filter tab UI: use "Channels" tab for Telegram/Discord, "Other" tab for Gmail/Notion (replaces old "Channel Integrations" / "3rd Party Skills" section headings) - Fix CTA button text: "Setup"/"Enable"/"Manage" instead of "Configure" - Fix Notion Enable click: use last matching XPath button (Notion appears after Gmail) to avoid clicking Gmail's Enable button - Fix Mac2 scroll: replace W3C wheel actions (unsupported) with native macos:scroll CGEvent in scrollDownInPage - Add TaskRecurrence::try_cron validated constructor; use it in RPC handler and DB deserialization with Pending fallback for corrupt data - Poll subconscious_log_list with timeout instead of fixed sleep in both E2E spec and json_rpc_e2e.rs integration test - Add missing-param validation test for cron_run (empty job_id) - Strengthen login-flow macOS fallback: assert isAuthenticated/token instead of just RPC reachability - Accept network errors in macos-distribution app_state_snapshot test (no mock server in that spec)
1 parent c233dc4 commit d153488

14 files changed

Lines changed: 478 additions & 227 deletions

‎app/test/e2e/helpers/element-helpers.ts‎

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ export async function hasAppChrome(): Promise<boolean> {
427427
/**
428428
* Scroll down inside the WebView / page by `amount` pixels.
429429
*
430-
* - Mac2: W3C wheel action centered on XCUIElementTypeWebView
430+
* - Mac2: native CGEvent scroll (macos: scroll) centered on XCUIElementTypeWebView
431431
* - tauri-driver: JS window.scrollBy
432432
*/
433433
export async function scrollDownInPage(amount: number = 400): Promise<void> {
@@ -440,7 +440,7 @@ export async function scrollDownInPage(amount: number = 400): Promise<void> {
440440
return;
441441
}
442442

443-
// Mac2: wheel action on the WebView center
443+
// Mac2: native CGEvent scroll via macos: scroll (same approach as scrollElementIntoViewMac2)
444444
try {
445445
const webView = await browser.$('//XCUIElementTypeWebView');
446446
if (await webView.isExisting()) {
@@ -449,16 +449,13 @@ export async function scrollDownInPage(amount: number = 400): Promise<void> {
449449
const centerX = Math.round(location.x + size.width / 2);
450450
const centerY = Math.round(location.y + size.height / 2);
451451

452-
await (browser as any).performActions([
453-
{
454-
type: 'wheel',
455-
id: 'scroll_wheel',
456-
actions: [
457-
{ type: 'scroll', x: centerX, y: centerY, deltaX: 0, deltaY: amount, duration: 300 },
458-
],
459-
},
460-
]);
461-
await (browser as any).releaseActions();
452+
// Negative deltaY scrolls page DOWN (more content from below appears)
453+
await browser.execute('macos: scroll', {
454+
x: centerX,
455+
y: centerY,
456+
deltaX: 0,
457+
deltaY: -amount,
458+
});
462459
await browser.pause(400);
463460
return;
464461
}

‎app/test/e2e/specs/automation-scheduling.spec.ts‎

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,55 @@ describe('Automation & Scheduling', () => {
137137
}
138138

139139
await expectRpcOk('openhuman.subconscious_trigger', {});
140-
if (methods.has('openhuman.subconscious_log_list')) {
141-
const logs = await expectRpcOk('openhuman.subconscious_log_list', { limit: 20 });
142-
expect(JSON.stringify(logs || {}).length > 2).toBe(true);
140+
141+
// Verify log entries were recorded — but only if the method exists in this build
142+
if (!methods.has('openhuman.subconscious_log_list')) {
143+
console.log(
144+
'[AutomationSpec] 6.2.2 — subconscious_log_list not in schema, skipping log verification'
145+
);
143146
return;
144147
}
145148

146-
await expectUnavailable('openhuman.subconscious_log_list', { limit: 20 });
149+
// Poll for log entries — the trigger may write asynchronously
150+
const deadline = Date.now() + 15_000;
151+
let lastResponse: unknown = null;
152+
let entries: unknown[] = [];
153+
154+
while (Date.now() < deadline) {
155+
const res = await callOpenhumanRpc('openhuman.subconscious_log_list', { limit: 20 });
156+
lastResponse = res;
157+
158+
if (res.ok) {
159+
const raw = res.result as Record<string, unknown>;
160+
const inner = Array.isArray(raw) ? raw : Array.isArray(raw?.result) ? raw.result : null;
161+
if (inner && inner.length > 0) {
162+
entries = inner;
163+
break;
164+
}
165+
} else if (typeof res.error === 'string' && res.error.includes('unknown method')) {
166+
// Method not available in running binary — skip
167+
console.log('[AutomationSpec] 6.2.2 — log_list unavailable at runtime, skipping');
168+
return;
169+
}
170+
await new Promise(r => setTimeout(r, 1_000));
171+
}
172+
173+
if (entries.length === 0) {
174+
console.log(
175+
'[AutomationSpec] 6.2.2 — log_list never returned entries.',
176+
'Last response:',
177+
JSON.stringify(lastResponse, null, 2)?.slice(0, 1000)
178+
);
179+
}
180+
expect(entries.length).toBeGreaterThan(0);
147181
});
148182

149183
it('6.2.3 — Missed Execution Handling: trigger endpoint remains safe across repeated calls', async () => {
184+
if (!methods.has('openhuman.subconscious_trigger')) {
185+
await expectUnavailable('openhuman.subconscious_trigger', {});
186+
return;
187+
}
188+
150189
await expectRpcOk('openhuman.subconscious_trigger', {});
151190
await expectRpcOk('openhuman.subconscious_trigger', {});
152191
});
@@ -156,17 +195,50 @@ describe('Automation & Scheduling', () => {
156195
await expectRpcOk('openhuman.cron_list', {});
157196
});
158197

159-
it('6.3.2 — Execution Trigger Handling: cron run with missing job_id fails explicitly', async () => {
160-
const res = await callOpenhumanRpc('openhuman.cron_run', { job_id: 'missing-job-id-e2e' });
161-
expect(res.ok).toBe(false);
198+
it('6.3.2 — Execution Trigger Handling: cron run validates job_id param', async () => {
199+
// Missing job_id — should hit parameter validation
200+
const missing = await callOpenhumanRpc('openhuman.cron_run', {});
201+
expect(missing.ok).toBe(false);
202+
expect(missing.error).toBeDefined();
203+
console.log('[AutomationSpec] 6.3.2 missing job_id error:', missing.error);
204+
205+
// Unknown job_id — should hit domain-level "not found"
206+
const unknown = await callOpenhumanRpc('openhuman.cron_run', { job_id: 'missing-job-id-e2e' });
207+
expect(unknown.ok).toBe(false);
208+
expect(unknown.error).toBeDefined();
209+
console.log('[AutomationSpec] 6.3.2 unknown job_id error:', unknown.error);
162210
});
163211

164212
it('6.3.3 — Failure Retry Logic: cron runs history endpoint remains queryable after failures', async () => {
213+
// Unknown job_id returns ok with empty runs array (DB has no entries)
165214
const runs = await callOpenhumanRpc('openhuman.cron_runs', {
166215
job_id: 'missing-job-id-e2e',
167216
limit: 5,
168217
});
169218

170-
expect(runs.ok || Boolean(runs.error)).toBe(true);
219+
if (runs.ok) {
220+
// Unwrap: result may be { result: [...], logs: [...] } or [...] directly
221+
const raw = runs.result as unknown;
222+
const entries = Array.isArray(raw)
223+
? raw
224+
: Array.isArray((raw as Record<string, unknown>)?.result)
225+
? (raw as Record<string, unknown>).result
226+
: null;
227+
console.log(
228+
'[AutomationSpec] 6.3.3 cron_runs ok, entries:',
229+
Array.isArray(entries) ? entries.length : 'not an array'
230+
);
231+
expect(Array.isArray(entries)).toBe(true);
232+
} else {
233+
// cron may be disabled — accept explicit error
234+
console.log('[AutomationSpec] 6.3.3 cron_runs failed:', runs.error);
235+
expect(runs.error).toBeDefined();
236+
}
237+
238+
// Empty job_id must fail validation
239+
const empty = await callOpenhumanRpc('openhuman.cron_runs', { job_id: '', limit: 5 });
240+
expect(empty.ok).toBe(false);
241+
expect(empty.error).toBeDefined();
242+
console.log('[AutomationSpec] 6.3.3 empty job_id error:', empty.error);
171243
});
172244
});

‎app/test/e2e/specs/chat-interface-flow.spec.ts‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,30 @@ describe('7. Chat Interface — RPC endpoint verification', () => {
107107

108108
it('7.2.1 — User Message Handling: web chat accepts user input payload', async () => {
109109
const res = await callOpenhumanRpc('openhuman.channel_web_chat', {
110-
input: 'hello from e2e',
111-
channel: 'web',
112-
target: 'e2e-thread-a',
110+
client_id: 'e2e-test-client',
111+
thread_id: 'e2e-thread-a',
112+
message: 'hello from e2e',
113113
});
114-
expect(res.ok || Boolean(res.error)).toBe(true);
114+
if (!res.ok) {
115+
// Chat may fail at runtime (no socket, no model) but should not fail at
116+
// param validation. A validation error contains "missing required param"
117+
// or "invalid" — that would be a real bug in the test payload.
118+
const isValidationError =
119+
typeof res.error === 'string' &&
120+
(res.error.includes('missing required param') || res.error.includes('invalid'));
121+
if (isValidationError) {
122+
console.log('[ChatInterfaceE2E] 7.2.1 VALIDATION ERROR — wrong payload shape:', res.error);
123+
} else {
124+
console.log('[ChatInterfaceE2E] 7.2.1 runtime error (expected in E2E):', res.error);
125+
}
126+
expect(isValidationError).toBe(false);
127+
} else {
128+
console.log(
129+
'[ChatInterfaceE2E] 7.2.1 web chat accepted payload:',
130+
JSON.stringify(res.result)
131+
);
132+
expect(res.ok).toBe(true);
133+
}
115134
});
116135

117136
it('7.2.2 — AI Response Generation: local_ai_agent_chat endpoint is registered', async () => {

‎app/test/e2e/specs/discord-flow.spec.ts‎

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ describe('8. Integrations (Discord) — RPC endpoint verification', () => {
8585
authMode: 'bot_token',
8686
credentials: { bot_token: 'fake-e2e-discord-token' },
8787
});
88-
expect(res.ok || Boolean(res.error)).toBe(true);
88+
if (!res.ok) {
89+
stepLog('8.1.1 channels_connect failed:', res.error);
90+
}
91+
expect(res.ok).toBe(true);
8992
});
9093

9194
it('8.1.2 — Scope Selection: channels_list returns Discord definition with capabilities', async () => {
@@ -101,7 +104,10 @@ describe('8. Integrations (Discord) — RPC endpoint verification', () => {
101104
});
102105
}
103106
}
104-
expect(res.ok || Boolean(res.error)).toBe(true);
107+
if (!res.ok) {
108+
stepLog('8.1.2 channels_list failed:', res.error);
109+
}
110+
expect(res.ok).toBe(true);
105111
});
106112

107113
it('8.1.3 — Token Storage: auth_store_provider_credentials registered', async () => {
@@ -115,7 +121,10 @@ describe('8. Integrations (Discord) — RPC endpoint verification', () => {
115121
it('8.2.1 — Read Access: channels_status returns Discord connection state', async () => {
116122
expectRpcMethod(methods, 'openhuman.channels_status');
117123
const res = await callOpenhumanRpc('openhuman.channels_status', { channel: 'discord' });
118-
expect(res.ok || Boolean(res.error)).toBe(true);
124+
if (!res.ok) {
125+
stepLog('8.2.1 channels_status failed:', res.error);
126+
}
127+
expect(res.ok).toBe(true);
119128
});
120129

121130
it('8.2.2 — Write Access: channels_send_message available', async () => {
@@ -156,7 +165,10 @@ describe('8. Integrations (Discord) — RPC endpoint verification', () => {
156165
channel: 'discord',
157166
authMode: 'bot_token',
158167
});
159-
expect(res.ok || Boolean(res.error)).toBe(true);
168+
if (!res.ok) {
169+
stepLog('8.4.1 channels_disconnect failed:', res.error);
170+
}
171+
expect(res.ok).toBe(true);
160172
});
161173

162174
it('8.4.2 — Token Revocation: auth_clear_session available', async () => {
@@ -173,12 +185,18 @@ describe('8. Integrations (Discord) — RPC endpoint verification', () => {
173185
authMode: 'bot_token',
174186
credentials: { bot_token: 'fake-e2e-discord-reauth' },
175187
});
176-
expect(res.ok || Boolean(res.error)).toBe(true);
188+
if (!res.ok) {
189+
stepLog('8.4.3 channels_connect (re-auth) failed:', res.error);
190+
}
191+
expect(res.ok).toBe(true);
177192
});
178193

179194
it('8.4.4 — Permission Re-Sync: channels_status refreshable after reconnect', async () => {
180195
const res = await callOpenhumanRpc('openhuman.channels_status', { channel: 'discord' });
181-
expect(res.ok || Boolean(res.error)).toBe(true);
196+
if (!res.ok) {
197+
stepLog('8.4.4 channels_status failed:', res.error);
198+
}
199+
expect(res.ok).toBe(true);
182200
});
183201
});
184202

@@ -231,7 +249,20 @@ describe('8.5 Integrations (Discord) — UI flow', () => {
231249
await navigateViaHash('/skills');
232250
await browser.pause(3_000);
233251

234-
// Discord card should be visible in Channel Integrations
252+
// Skills page uses filter tabs (All, Built-in, Channels, Other).
253+
// Click the "Channels" tab to show channel cards.
254+
const hasChannelsTab = await textExists('Channels');
255+
if (hasChannelsTab) {
256+
try {
257+
await clickText('Channels', 8_000);
258+
await browser.pause(2_000);
259+
stepLog('Clicked "Channels" filter tab');
260+
} catch {
261+
stepLog('Could not click Channels tab — continuing with All view');
262+
}
263+
}
264+
265+
// Discord card should now be visible
235266
const hasDiscord = await textExists('Discord');
236267
if (!hasDiscord) {
237268
const tree = await dumpAccessibilityTree();
@@ -241,37 +272,29 @@ describe('8.5 Integrations (Discord) — UI flow', () => {
241272
stepLog('Discord channel visible on Skills page');
242273
});
243274

244-
it('8.5.2 — Discord card shows status and Configure button', async () => {
245-
// Description: "Send and receive messages via Discord."
275+
it('8.5.2 — Discord card shows status and action button', async () => {
246276
const hasDescription = await textExists('Send and receive messages via Discord');
247277
stepLog('Discord card description', { visible: hasDescription });
248278

249-
const hasConfigure = await textExists('Configure');
250-
expect(hasConfigure).toBe(true);
251-
252-
// Status: Connected, Not configured, Connecting, Error
253-
const hasConnected = await textExists('Connected');
254-
const hasNotConfigured = await textExists('Not configured');
255-
const hasStatus =
256-
hasConnected ||
257-
hasNotConfigured ||
258-
(await textExists('Connecting')) ||
259-
(await textExists('Error'));
260-
stepLog('Discord status', { connected: hasConnected, notConfigured: hasNotConfigured });
261-
expect(hasStatus).toBe(true);
279+
// CTA button: "Setup" (disconnected) or "Manage" (connected)
280+
const hasSetup = await textExists('Setup');
281+
const hasManage = await textExists('Manage');
282+
const hasCta = hasSetup || hasManage;
283+
stepLog('Discord CTA', { setup: hasSetup, manage: hasManage });
284+
expect(hasCta).toBe(true);
262285
});
263286

264-
it('8.5.3 — Click Discord Configure opens modal with auth modes and fields', async () => {
265-
// Click the Discord card (click "Discord" text — the whole card is a button)
266-
stepLog('clicking Discord card');
287+
it('8.5.3 — Click Discord Setup opens modal with auth modes and fields', async () => {
288+
// Click the CTA button to open the ChannelSetupModal
289+
stepLog('clicking Discord card to open Setup modal');
267290
try {
268-
// There are two "Configure" buttons (Telegram + Discord) — click "Discord" directly
269-
await clickText('Discord', 10_000);
291+
await clickText('Setup', 10_000);
270292
} catch {
271-
// Fallback: scroll to find it
272-
const { scrollToFindText } = await import('../helpers/element-helpers');
273-
await scrollToFindText('Discord');
274-
await clickText('Discord', 10_000);
293+
try {
294+
await clickText('Manage', 10_000);
295+
} catch {
296+
await clickText('Discord', 10_000);
297+
}
275298
}
276299
await browser.pause(3_000);
277300

0 commit comments

Comments
 (0)