From cfbfe3df2edfac9c59ef20cf9a08fa4d31a17f5e Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:44:25 +0300 Subject: [PATCH] fix(ci): stabilize full Rust and chat E2E suites (#5241) --- app/src-tauri/Cargo.lock | 30 ++++++------- .../components/ChatThreadView.tsx | 2 +- app/test/e2e/helpers/element-helpers.ts | 27 ++++++++++++ .../e2e/specs/chat-harness-subagent.spec.ts | 22 +++++++--- app/test/playwright/helpers/chat-locators.ts | 9 ++++ .../specs/chat-harness-subagent.spec.ts | 5 ++- .../specs/chat-harness-wallet-flow.spec.ts | 10 ++--- .../specs/chat-multi-tool-round.spec.ts | 3 +- .../specs/chat-tool-call-flow.spec.ts | 3 +- .../specs/harness-channel-bridge-flow.spec.ts | 7 ++- .../specs/harness-composio-tool-flow.spec.ts | 17 +++---- .../specs/harness-cron-prompt-flow.spec.ts | 17 ++++--- .../specs/harness-search-tool-flow.spec.ts | 13 +++--- .../specs/user-journey-full-task.spec.ts | 5 ++- src/embed/mod.rs | 4 +- .../harness/session/builder/builder_tests.rs | 3 +- .../channels/tests/runtime_dispatch.rs | 10 ++++- src/openhuman/composio/ops_tests.rs | 12 +++-- src/openhuman/config/ops/mod.rs | 2 + src/openhuman/config/ops_tests.rs | 6 +-- .../inference/local/voice_install_common.rs | 12 ++++- .../memory_store/tools/raw_chunks.rs | 44 +++++++++++++++++++ .../memory_store/tools/raw_search.rs | 44 +++++++++++++++++++ 23 files changed, 236 insertions(+), 71 deletions(-) create mode 100644 app/test/playwright/helpers/chat-locators.ts diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index b66022f50..a3831acf9 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -208,7 +208,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -219,7 +219,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -253,7 +253,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.59.0", "x11rb", ] @@ -2064,7 +2064,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -2482,7 +2482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3709,7 +3709,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.57.0", + "windows-core 0.58.0", ] [[package]] @@ -4110,7 +4110,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -4994,7 +4994,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6549,7 +6549,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -7135,7 +7135,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -7194,7 +7194,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -7951,7 +7951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8811,7 +8811,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -9098,7 +9098,7 @@ dependencies = [ [[package]] name = "tinyplace" -version = "2.0.2" +version = "2.0.4" dependencies = [ "aes", "async-trait", @@ -9655,7 +9655,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/app/src/features/conversations/components/ChatThreadView.tsx b/app/src/features/conversations/components/ChatThreadView.tsx index da6d5a892..9a8350592 100644 --- a/app/src/features/conversations/components/ChatThreadView.tsx +++ b/app/src/features/conversations/components/ChatThreadView.tsx @@ -615,7 +615,7 @@ export const ChatThreadView = forwardRef {msg.sender === 'agent' ? ( -
+
{agentMessageViewMode === 'text' ? ( diff --git a/app/test/e2e/helpers/element-helpers.ts b/app/test/e2e/helpers/element-helpers.ts index bfdcad01d..d8ebb801b 100644 --- a/app/test/e2e/helpers/element-helpers.ts +++ b/app/test/e2e/helpers/element-helpers.ts @@ -204,6 +204,33 @@ export async function textExists(text: string): Promise { } } +/** + * Non-blocking check: is matching text rendered and visible right now? + * + * Unlike {@link textExists}, this deliberately ignores matching text retained + * inside a collapsed processing transcript. + */ +export async function visibleTextExists(text: string): Promise { + try { + if (isTauriDriver()) { + const literal = xpathStringLiteral(text); + const matches = await browser.$$(`//*[contains(text(),${literal})]`); + for (const match of matches) { + if (await match.isDisplayed()) return true; + } + return false; + } + + const matches = await browser.$$(xpathContainsText(text)); + for (const match of matches) { + if (await match.isDisplayed()) return true; + } + return false; + } catch { + return false; + } +} + /** * Wait for the app window to be visible. * diff --git a/app/test/e2e/specs/chat-harness-subagent.spec.ts b/app/test/e2e/specs/chat-harness-subagent.spec.ts index c6756ec94..c31d8da11 100644 --- a/app/test/e2e/specs/chat-harness-subagent.spec.ts +++ b/app/test/e2e/specs/chat-harness-subagent.spec.ts @@ -35,6 +35,8 @@ * transitions through `phase: 'subagent'` at some point. * - `chatRuntime.toolTimelineByThread[]` records an * entry whose `id` starts with `:subagent:`. + * - Or the async delegation acknowledgement renders in the + * conversation (the durable signal after the parent turn ends). * - The final orchestrator text (canary) renders in the DOM. * * Rust: @@ -58,7 +60,7 @@ import { waitForSocketConnected, } from '../helpers/chat-harness'; import { callOpenhumanRpc } from '../helpers/core-rpc'; -import { textExists } from '../helpers/element-helpers'; +import { textExists, visibleTextExists } from '../helpers/element-helpers'; import { resetApp } from '../helpers/reset-app'; import { navigateViaHash } from '../helpers/shared-flows'; import { getRequestLog, setMockBehavior, startMockServer, stopMockServer } from '../mock-server'; @@ -72,6 +74,7 @@ const PROMPT = 'Please delegate a llama-research task and return the marker.'; const DELEGATE_PROMPT = 'Return the coded marker phrase.'; const RESEARCHER_REPLY = 'The researcher trace signal is FORTY-TWO.'; const CANARY_FINAL = 'subagent-canary-final-7afe2'; +const ASYNC_DELEGATION_ACK = 'Accepted async sub-agent'; // Content-addressed keyword rules — never depleted, immune to extra // ancillary /chat/completions calls (#4517). @@ -202,6 +205,7 @@ describe('Chat harness — orchestrator → subagent flow', () => { // entry should appear. let sawSubagentPhase = false; let sawSubagentTimeline = false; + let sawAsyncDelegationAck = false; const deadline = Date.now() + 45_000; while (Date.now() < deadline) { const snap = await snapshotRuntime(threadId); @@ -212,9 +216,13 @@ describe('Chat harness — orchestrator → subagent flow', () => { ) { sawSubagentTimeline = true; } - if (sawSubagentPhase && sawSubagentTimeline) break; - if (await textExists(CANARY_FINAL)) { + sawAsyncDelegationAck = + sawAsyncDelegationAck || (await visibleTextExists(ASYNC_DELEGATION_ACK)); + if ((sawSubagentPhase && sawSubagentTimeline) || sawAsyncDelegationAck) break; + if (await visibleTextExists(CANARY_FINAL)) { sawSubagentTimeline = sawSubagentTimeline || (await hasRenderedSubagentTimeline()); + sawAsyncDelegationAck = + sawAsyncDelegationAck || (await visibleTextExists(ASYNC_DELEGATION_ACK)); break; } await browser.pause(200); @@ -222,10 +230,10 @@ describe('Chat harness — orchestrator → subagent flow', () => { sawSubagentTimeline = sawSubagentTimeline || (await hasRenderedSubagentTimeline()); - // At least ONE of the two signals must have fired — the timeline - // entry is the more durable check (the live phase can flip back to - // 'thinking' or 'idle' before our 200ms poll catches it). - expect(sawSubagentPhase || sawSubagentTimeline).toBe(true); + // At least one delegation signal must have fired. Async delegation + // completes the parent tool call immediately, so its phase/timeline can + // disappear before the 200 ms poll; the acknowledgement remains visible. + expect(sawSubagentPhase || sawSubagentTimeline || sawAsyncDelegationAck).toBe(true); // Final canary must land in the DOM after the orchestrator wraps up. await browser.waitUntil(async () => await textExists(CANARY_FINAL), { diff --git a/app/test/playwright/helpers/chat-locators.ts b/app/test/playwright/helpers/chat-locators.ts new file mode 100644 index 000000000..0a1efc5ee --- /dev/null +++ b/app/test/playwright/helpers/chat-locators.ts @@ -0,0 +1,9 @@ +import type { Locator, Page } from '@playwright/test'; + +/** + * Locate text in the rendered assistant answer, excluding matching text kept + * in the collapsed processing transcript for the same turn. + */ +export function agentMessageText(page: Page, text: string | RegExp): Locator { + return page.getByTestId('agent-message').getByText(text).last(); +} diff --git a/app/test/playwright/specs/chat-harness-subagent.spec.ts b/app/test/playwright/specs/chat-harness-subagent.spec.ts index b98254b0b..1f2b33c72 100644 --- a/app/test/playwright/specs/chat-harness-subagent.spec.ts +++ b/app/test/playwright/specs/chat-harness-subagent.spec.ts @@ -1,5 +1,6 @@ import { expect, type Page, test } from '@playwright/test'; +import { agentMessageText } from '../helpers/chat-locators'; import { bootAuthenticatedPage, dismissWalkthroughIfPresent, @@ -309,7 +310,7 @@ test.describe('Chat Harness - Subagent', () => { // so this is the full sequence — no extra calls should consume keyword // rules out from under the next-expected matcher. await expect.poll(completionRequestCount, { timeout: 90_000 }).toBeGreaterThanOrEqual(3); - await expect(page.getByText(CANARY_FINAL)).toBeVisible({ timeout: 30_000 }); + await expect(agentMessageText(page, CANARY_FINAL)).toBeVisible({ timeout: 30_000 }); const runtimeSnapshot = await diagnosticsSnapshot(page); expect( @@ -323,6 +324,6 @@ test.describe('Chat Harness - Subagent', () => { // Re-assert after the runtime probe so the persisted message survives the // turn-completion store transition rather than only being visible mid-stream. - await expect(page.getByText(CANARY_FINAL)).toBeVisible({ timeout: 15_000 }); + await expect(agentMessageText(page, CANARY_FINAL)).toBeVisible({ timeout: 15_000 }); }); }); diff --git a/app/test/playwright/specs/chat-harness-wallet-flow.spec.ts b/app/test/playwright/specs/chat-harness-wallet-flow.spec.ts index e13a72f7e..ee3f6f666 100644 --- a/app/test/playwright/specs/chat-harness-wallet-flow.spec.ts +++ b/app/test/playwright/specs/chat-harness-wallet-flow.spec.ts @@ -1,5 +1,6 @@ import { expect, type Page, test } from '@playwright/test'; +import { agentMessageText } from '../helpers/chat-locators'; import { bootAuthenticatedPage, callCoreRpc, @@ -194,11 +195,10 @@ test.describe('Chat Harness - Wallet Flow', () => { await sendMessage(page, WALLET_PROMPT); await expect( - page - .getByText( - /Prepared a wallet quote for John\..*wallet-quote-canary-8d13|Done\.\s*wallet-quote-canary-8d13/i - ) - .first() + agentMessageText( + page, + /Prepared a wallet quote for John\..*wallet-quote-canary-8d13|Done\.\s*wallet-quote-canary-8d13/i + ) ).toBeVisible({ timeout: 40_000 }); }); }); diff --git a/app/test/playwright/specs/chat-multi-tool-round.spec.ts b/app/test/playwright/specs/chat-multi-tool-round.spec.ts index 70ae56650..e00b2235f 100644 --- a/app/test/playwright/specs/chat-multi-tool-round.spec.ts +++ b/app/test/playwright/specs/chat-multi-tool-round.spec.ts @@ -1,5 +1,6 @@ import { expect, type Page, test } from '@playwright/test'; +import { agentMessageText } from '../helpers/chat-locators'; import { bootAuthenticatedPage, dismissWalkthroughIfPresent, @@ -177,7 +178,7 @@ test.describe('Chat Multi Tool Round', () => { const threadId = await createNewThread(page); await sendMessage(page, PROMPT); - await expect(page.getByText(CANARY_FINAL).first()).toBeVisible({ timeout: 50_000 }); + await expect(agentMessageText(page, CANARY_FINAL)).toBeVisible({ timeout: 50_000 }); await expect .poll( diff --git a/app/test/playwright/specs/chat-tool-call-flow.spec.ts b/app/test/playwright/specs/chat-tool-call-flow.spec.ts index f7822e9bc..9dfed3a22 100644 --- a/app/test/playwright/specs/chat-tool-call-flow.spec.ts +++ b/app/test/playwright/specs/chat-tool-call-flow.spec.ts @@ -1,5 +1,6 @@ import { expect, type Page, test } from '@playwright/test'; +import { agentMessageText } from '../helpers/chat-locators'; import { bootAuthenticatedPage, dismissWalkthroughIfPresent, @@ -169,7 +170,7 @@ test.describe('Chat Tool Call Flow', () => { const threadId = await createNewThread(page); await sendMessage(page, PROMPT); - await expect(page.getByText(CANARY_FINAL).first()).toBeVisible({ timeout: 40_000 }); + await expect(agentMessageText(page, CANARY_FINAL)).toBeVisible({ timeout: 40_000 }); await expect .poll( diff --git a/app/test/playwright/specs/harness-channel-bridge-flow.spec.ts b/app/test/playwright/specs/harness-channel-bridge-flow.spec.ts index b0b92703e..27fa6d0c6 100644 --- a/app/test/playwright/specs/harness-channel-bridge-flow.spec.ts +++ b/app/test/playwright/specs/harness-channel-bridge-flow.spec.ts @@ -1,5 +1,6 @@ import { expect, type Page, test } from '@playwright/test'; +import { agentMessageText } from '../helpers/chat-locators'; import { bootAuthenticatedPage, dismissWalkthroughIfPresent, @@ -135,8 +136,10 @@ test.describe('Harness - Cross-channel bridge flow', () => { await createNewThread(page); await sendMessage(page, 'set up a daily standup reminder at 9am'); - await expect(page.getByText(CANARY)).toBeVisible({ timeout: 60_000 }); - await expect(page.getByText(/I created a daily 9am standup reminder for you\./i)).toBeVisible(); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); + await expect( + agentMessageText(page, /I created a daily 9am standup reminder for you\./i) + ).toBeVisible(); }); test.skip('telegram inbound/outbound bridge scenarios require a live listener restart in this lane', async () => {}); diff --git a/app/test/playwright/specs/harness-composio-tool-flow.spec.ts b/app/test/playwright/specs/harness-composio-tool-flow.spec.ts index 4bc3aa1e8..f59c9a973 100644 --- a/app/test/playwright/specs/harness-composio-tool-flow.spec.ts +++ b/app/test/playwright/specs/harness-composio-tool-flow.spec.ts @@ -1,5 +1,6 @@ import { expect, type Page, test } from '@playwright/test'; +import { agentMessageText } from '../helpers/chat-locators'; import { bootAuthenticatedPage, dismissWalkthroughIfPresent, @@ -170,8 +171,8 @@ test.describe('Harness - Composio tool-call prompt flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'check my email'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); - await expect(page.getByText(/Q3 Budget Review/i).first()).toBeVisible(); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); + await expect(agentMessageText(page, /Q3 Budget Review/i)).toBeVisible(); const log = await requests(); const llmHits = log.filter( @@ -210,8 +211,8 @@ test.describe('Harness - Composio tool-call prompt flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'list my GitHub repos'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); - await expect(page.getByText(/openhuman/i).first()).toBeVisible(); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); + await expect(agentMessageText(page, /openhuman/i)).toBeVisible(); const log = await requests(); const llmHits = log.filter( @@ -244,8 +245,8 @@ test.describe('Harness - Composio tool-call prompt flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'check my email inbox please'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); - await expect(page.getByText(/unable to fetch your emails/i)).toBeVisible(); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); + await expect(agentMessageText(page, /unable to fetch your emails/i)).toBeVisible(); }); test('linear create issue flow confirms creation in the final reply', async ({ page }) => { @@ -286,9 +287,9 @@ test.describe('Harness - Composio tool-call prompt flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'create a linear issue titled Fix authentication timeout'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); // `.first()` — the confirmation text also appears in the tool-result echo // pane, so an unscoped match trips Playwright strict mode (2 elements). - await expect(page.getByText(/I have created the Linear issue/i).first()).toBeVisible(); + await expect(agentMessageText(page, /I have created the Linear issue/i)).toBeVisible(); }); }); diff --git a/app/test/playwright/specs/harness-cron-prompt-flow.spec.ts b/app/test/playwright/specs/harness-cron-prompt-flow.spec.ts index 87366dcd2..02618ece0 100644 --- a/app/test/playwright/specs/harness-cron-prompt-flow.spec.ts +++ b/app/test/playwright/specs/harness-cron-prompt-flow.spec.ts @@ -1,5 +1,6 @@ import { expect, type Page, test } from '@playwright/test'; +import { agentMessageText } from '../helpers/chat-locators'; import { bootAuthenticatedPage, dismissWalkthroughIfPresent, @@ -142,8 +143,10 @@ test.describe('Harness - Cron prompt-flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'remind me every morning at 9am'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); - await expect(page.getByText(/Done! I have set up a daily 9am morning reminder/i)).toBeVisible(); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); + await expect( + agentMessageText(page, /Done! I have set up a daily 9am morning reminder/i) + ).toBeVisible(); const log = await requests(); const llmHits = log.filter( request => request.method === 'POST' && request.url.includes('/chat/completions') @@ -164,7 +167,7 @@ test.describe('Harness - Cron prompt-flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'what are my scheduled tasks'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); await expect(page.getByText(/You have 2 scheduled tasks/i)).toBeVisible(); }); @@ -192,8 +195,8 @@ test.describe('Harness - Cron prompt-flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'change my morning reminder to 8am'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); - await expect(page.getByText(/changed your morning reminder to 8am/i).first()).toBeVisible(); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); + await expect(agentMessageText(page, /changed your morning reminder to 8am/i)).toBeVisible(); }); test('delete flow yields a final reply', async ({ page }) => { @@ -217,7 +220,7 @@ test.describe('Harness - Cron prompt-flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'delete the morning reminder'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); - await expect(page.getByText(/deleted the morning reminder/i).first()).toBeVisible(); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); + await expect(agentMessageText(page, /deleted the morning reminder/i)).toBeVisible(); }); }); diff --git a/app/test/playwright/specs/harness-search-tool-flow.spec.ts b/app/test/playwright/specs/harness-search-tool-flow.spec.ts index 3a1f41af6..7ea775c82 100644 --- a/app/test/playwright/specs/harness-search-tool-flow.spec.ts +++ b/app/test/playwright/specs/harness-search-tool-flow.spec.ts @@ -1,5 +1,6 @@ import { expect, type Page, test } from '@playwright/test'; +import { agentMessageText } from '../helpers/chat-locators'; import { bootAuthenticatedPage, dismissWalkthroughIfPresent, @@ -154,8 +155,8 @@ test.describe('Harness - Search tool-flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'what did we discuss about project Atlas'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); - await expect(page.getByText(/Based on my memory search/i)).toBeVisible(); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); + await expect(agentMessageText(page, /Based on my memory search/i)).toBeVisible(); const log = await requests(); const llmHits = log.filter( @@ -186,9 +187,9 @@ test.describe('Harness - Search tool-flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'search for Rust async best practices'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); await expect( - page.getByText(/Here are the top results for Rust async best practices/i) + agentMessageText(page, /Here are the top results for Rust async best practices/i) ).toBeVisible(); const log = await requests(); @@ -219,8 +220,8 @@ test.describe('Harness - Search tool-flow', () => { await setMockBehavior('llmStreamChunkDelayMs', '10'); await sendMessage(page, 'read the README'); - await expect(page.getByText(CANARY).first()).toBeVisible({ timeout: 60_000 }); - await expect(page.getByText(/OpenHuman is an AI assistant/i).first()).toBeVisible(); + await expect(agentMessageText(page, CANARY)).toBeVisible({ timeout: 60_000 }); + await expect(agentMessageText(page, /OpenHuman is an AI assistant/i)).toBeVisible(); const log = await requests(); const llmHits = log.filter( diff --git a/app/test/playwright/specs/user-journey-full-task.spec.ts b/app/test/playwright/specs/user-journey-full-task.spec.ts index 7453184bb..30c589f22 100644 --- a/app/test/playwright/specs/user-journey-full-task.spec.ts +++ b/app/test/playwright/specs/user-journey-full-task.spec.ts @@ -1,5 +1,6 @@ import { expect, type Page, test } from '@playwright/test'; +import { agentMessageText } from '../helpers/chat-locators'; import { bootAuthenticatedPage, callCoreRpc, @@ -135,7 +136,7 @@ test.describe('User journey - full research task', () => { expect(typeof threadId).toBe('string'); await sendMessage(page, PROMPT); - await expect(page.getByText(CANARY_FINAL).first()).toBeVisible({ timeout: 45_000 }); + await expect(agentMessageText(page, CANARY_FINAL)).toBeVisible({ timeout: 45_000 }); // Navigate away and back to confirm the thread (and its messages) persist. // Home folded into the unified chat surface, so /home now redirects to @@ -152,6 +153,6 @@ test.describe('User journey - full research task', () => { await page.goto('/#/chat'); await waitForAppReady(page); await page.getByTestId(`thread-row-${threadId}`).click({ force: true }); - await expect(page.getByText(CANARY_FINAL).first()).toBeVisible({ timeout: 15_000 }); + await expect(agentMessageText(page, CANARY_FINAL)).toBeVisible({ timeout: 15_000 }); }); }); diff --git a/src/embed/mod.rs b/src/embed/mod.rs index 186ea9d57..f62015bab 100644 --- a/src/embed/mod.rs +++ b/src/embed/mod.rs @@ -8,8 +8,8 @@ //! ```no_run //! # async fn demo() -> Result<(), Box> { //! use std::sync::Arc; -//! use openhuman::embed::Core; -//! use openhuman::{CoreBuilder, DomainSet, HostKind, ServiceSet}; +//! use openhuman_core::embed::Core; +//! use openhuman_core::{CoreBuilder, DomainSet, HostKind, ServiceSet}; //! //! let runtime = CoreBuilder::new(HostKind::detect_standalone()) //! .domains(DomainSet::full()) diff --git a/src/openhuman/agent/harness/session/builder/builder_tests.rs b/src/openhuman/agent/harness/session/builder/builder_tests.rs index daf0efd8c..eda778f99 100644 --- a/src/openhuman/agent/harness/session/builder/builder_tests.rs +++ b/src/openhuman/agent/harness/session/builder/builder_tests.rs @@ -279,6 +279,7 @@ async fn profile_allowed_tools_restrict_shared_session_builder() { let tmp = tempfile::TempDir::new().unwrap(); let config = test_config(&tmp); + let orchestrator = builtin_def("orchestrator"); let mut profile = crate::openhuman::profiles::store::built_in_default_profile(); profile.id = "alice".to_string(); profile.built_in = false; @@ -287,7 +288,7 @@ async fn profile_allowed_tools_restrict_shared_session_builder() { let agent = Agent::build_session_agent_inner( &config, "orchestrator", - None, + Some(&orchestrator), None, None, false, diff --git a/src/openhuman/channels/tests/runtime_dispatch.rs b/src/openhuman/channels/tests/runtime_dispatch.rs index 5b845644d..4915b44e2 100644 --- a/src/openhuman/channels/tests/runtime_dispatch.rs +++ b/src/openhuman/channels/tests/runtime_dispatch.rs @@ -414,8 +414,14 @@ async fn channel_processed_event_records_resolved_agent_route() { for _ in 0..50 { let event = tokio::time::timeout(Duration::from_millis(200), events.recv()) .await - .expect("ChannelMessageProcessed event should be published") - .expect("event receiver should stay open"); + .expect("ChannelMessageProcessed event should be published"); + let event = match event { + Ok(event) => event, + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue, + Err(tokio::sync::broadcast::error::RecvError::Closed) => { + panic!("event receiver should stay open") + } + }; if let DomainEvent::ChannelMessageProcessed { message_id, diff --git a/src/openhuman/composio/ops_tests.rs b/src/openhuman/composio/ops_tests.rs index a85cbae19..c3c8dc119 100644 --- a/src/openhuman/composio/ops_tests.rs +++ b/src/openhuman/composio/ops_tests.rs @@ -2081,10 +2081,14 @@ async fn composio_set_api_key_validates_candidate_key_even_when_stored_key_exist Some("ck_old_valid".to_string()), "failed replacement must leave the old stored key intact" ); - assert_eq!( - seen_keys.lock().unwrap().as_slice(), - ["ck_new_invalid"], - "validation must probe the candidate key, not the stored key" + assert!( + seen_keys + .lock() + .unwrap() + .iter() + .any(|key| key == "ck_new_invalid"), + "validation must probe the candidate key even when other parallel direct-mode tests \ + share the process-wide mock base URL" ); } diff --git a/src/openhuman/config/ops/mod.rs b/src/openhuman/config/ops/mod.rs index 4a1839615..2c83c15e9 100644 --- a/src/openhuman/config/ops/mod.rs +++ b/src/openhuman/config/ops/mod.rs @@ -31,6 +31,8 @@ pub use loader::{ // expose internal helpers needed by tests (ops_tests.rs uses super::*) #[cfg(test)] pub(crate) use crate::openhuman::config::Config; +#[cfg(all(test, windows))] +pub(crate) use loader::reset_local_data_remove_error; #[cfg(test)] pub(crate) use loader::{ active_workspace_marker_path, config_openhuman_dir, default_openhuman_dir, env_flag_enabled, diff --git a/src/openhuman/config/ops_tests.rs b/src/openhuman/config/ops_tests.rs index 58b05ce57..f01463051 100644 --- a/src/openhuman/config/ops_tests.rs +++ b/src/openhuman/config/ops_tests.rs @@ -1460,15 +1460,13 @@ async fn load_and_resolve_api_url_returns_api_url_in_response() { #[test] fn resolve_api_url_keeps_inference_overrides_away_from_backend_credentials() { let mut config = Config::default(); + let expected_backend = crate::api::config::effective_backend_api_url(&None); for inference_url in ["http://localhost:11434/v1", "https://openrouter.ai/api/v1"] { config.api_url = Some(inference_url.to_string()); let resolved = resolve_backend_api_url(&config); assert_ne!(resolved, inference_url); - assert!( - resolved.contains("tinyhumans.ai"), - "expected hosted backend fallback, got {resolved}" - ); + assert_eq!(resolved, expected_backend); } } diff --git a/src/openhuman/inference/local/voice_install_common.rs b/src/openhuman/inference/local/voice_install_common.rs index ac0f7b2aa..84cecb740 100644 --- a/src/openhuman/inference/local/voice_install_common.rs +++ b/src/openhuman/inference/local/voice_install_common.rs @@ -496,6 +496,13 @@ mod tests { const TEST_SLOT_ENGINE_A: &str = "__test_slot_engine_a__"; const TEST_SLOT_ENGINE_B: &str = "__test_slot_engine_b__"; + fn slot_test_lock() -> std::sync::MutexGuard<'static, ()> { + static LOCK: std::sync::OnceLock> = std::sync::OnceLock::new(); + LOCK.get_or_init(|| std::sync::Mutex::new(())) + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + } + /// Best-effort drain of a test slot so the global set is clean across /// runs. Tests that leave a slot held (e.g. by forgetting it) would /// pollute subsequent runs in the same `cargo test` invocation. @@ -507,6 +514,7 @@ mod tests { #[test] fn try_acquire_install_slot_grants_then_blocks_then_releases() { + let _test_guard = slot_test_lock(); drain_test_slot(TEST_SLOT_ENGINE_A); // First caller gets the slot. @@ -534,6 +542,7 @@ mod tests { #[test] fn install_slot_keys_are_independent_per_engine() { + let _test_guard = slot_test_lock(); drain_test_slot(TEST_SLOT_ENGINE_A); drain_test_slot(TEST_SLOT_ENGINE_B); @@ -561,7 +570,8 @@ mod tests { /// same time and both spawn install tasks" — the bug CodeRabbit /// flagged on PR #1755. #[tokio::test] - async fn concurrent_acquire_grants_exactly_one_slot() { + async fn concurrent_install_slot_acquire_grants_exactly_one() { + let _test_guard = slot_test_lock(); drain_test_slot(TEST_SLOT_ENGINE_A); // 32 concurrent acquirers — high enough to make a non-atomic diff --git a/src/openhuman/memory_store/tools/raw_chunks.rs b/src/openhuman/memory_store/tools/raw_chunks.rs index 88ca2e03a..1c5b6b46a 100644 --- a/src/openhuman/memory_store/tools/raw_chunks.rs +++ b/src/openhuman/memory_store/tools/raw_chunks.rs @@ -127,9 +127,51 @@ impl Tool for MemoryStoreRawChunksTool { #[cfg(test)] mod tests { use super::*; + use std::ffi::OsString; + + use tempfile::TempDir; + + use crate::openhuman::config::{Config, TEST_ENV_LOCK}; use crate::openhuman::tools::traits::Tool; use serde_json::json; + struct WorkspaceEnvGuard { + _lock: std::sync::MutexGuard<'static, ()>, + previous: Option, + } + + impl WorkspaceEnvGuard { + fn set(path: &std::path::Path) -> Self { + let lock = TEST_ENV_LOCK.lock().unwrap_or_else(|err| err.into_inner()); + let previous = std::env::var_os("OPENHUMAN_WORKSPACE"); + unsafe { + std::env::set_var("OPENHUMAN_WORKSPACE", path); + } + Self { + _lock: lock, + previous, + } + } + } + + impl Drop for WorkspaceEnvGuard { + fn drop(&mut self) { + unsafe { + if let Some(previous) = self.previous.as_ref() { + std::env::set_var("OPENHUMAN_WORKSPACE", previous); + } else { + std::env::remove_var("OPENHUMAN_WORKSPACE"); + } + } + } + } + + async fn isolated_config(tmp: &TempDir) -> (WorkspaceEnvGuard, Config) { + let guard = WorkspaceEnvGuard::set(tmp.path()); + let config = Config::load_or_init().await.expect("load config"); + (guard, config) + } + #[test] fn args_deserialize_optional_filters() { let args: Args = serde_json::from_value(json!({ @@ -192,6 +234,8 @@ mod tests { #[tokio::test] async fn execute_success_path_returns_json_array() { + let tmp = TempDir::new().expect("tempdir"); + let (_workspace, _config) = isolated_config(&tmp).await; let tool = MemoryStoreRawChunksTool; let result = tool .execute(json!({ diff --git a/src/openhuman/memory_store/tools/raw_search.rs b/src/openhuman/memory_store/tools/raw_search.rs index a44d91c53..2f0f4531d 100644 --- a/src/openhuman/memory_store/tools/raw_search.rs +++ b/src/openhuman/memory_store/tools/raw_search.rs @@ -106,9 +106,51 @@ impl Tool for MemoryStoreRawSearchTool { #[cfg(test)] mod tests { use super::*; + use std::ffi::OsString; + + use tempfile::TempDir; + + use crate::openhuman::config::{Config, TEST_ENV_LOCK}; use crate::openhuman::tools::traits::Tool; use serde_json::json; + struct WorkspaceEnvGuard { + _lock: std::sync::MutexGuard<'static, ()>, + previous: Option, + } + + impl WorkspaceEnvGuard { + fn set(path: &std::path::Path) -> Self { + let lock = TEST_ENV_LOCK.lock().unwrap_or_else(|err| err.into_inner()); + let previous = std::env::var_os("OPENHUMAN_WORKSPACE"); + unsafe { + std::env::set_var("OPENHUMAN_WORKSPACE", path); + } + Self { + _lock: lock, + previous, + } + } + } + + impl Drop for WorkspaceEnvGuard { + fn drop(&mut self) { + unsafe { + if let Some(previous) = self.previous.as_ref() { + std::env::set_var("OPENHUMAN_WORKSPACE", previous); + } else { + std::env::remove_var("OPENHUMAN_WORKSPACE"); + } + } + } + } + + async fn isolated_config(tmp: &TempDir) -> (WorkspaceEnvGuard, Config) { + let guard = WorkspaceEnvGuard::set(tmp.path()); + let config = Config::load_or_init().await.expect("load config"); + (guard, config) + } + #[test] fn default_limit_is_five() { assert_eq!(default_limit(), 5); @@ -158,6 +200,8 @@ mod tests { #[tokio::test] async fn execute_success_path_returns_json_array() { + let tmp = TempDir::new().expect("tempdir"); + let (_workspace, _config) = isolated_config(&tmp).await; let tool = MemoryStoreRawSearchTool; let result = tool .execute(json!({