mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 22:23:01 +00:00
fix(ci): stabilize full Rust and chat E2E suites (#5241)
This commit is contained in:
Generated
+15
-15
@@ -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]]
|
||||
|
||||
@@ -615,7 +615,7 @@ export const ChatThreadView = forwardRef<ChatThreadViewHandle, ChatThreadViewPro
|
||||
isAgentTextMode ? 'w-full max-w-full' : 'w-fit max-w-[75%]'
|
||||
}`}>
|
||||
{msg.sender === 'agent' ? (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1" data-testid="agent-message">
|
||||
<div className="relative space-y-1">
|
||||
{agentMessageViewMode === 'text' ? (
|
||||
<AgentMessageText content={displayContent} />
|
||||
|
||||
@@ -204,6 +204,33 @@ export async function textExists(text: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<boolean> {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
* transitions through `phase: 'subagent'` at some point.
|
||||
* - `chatRuntime.toolTimelineByThread[<thread>]` records an
|
||||
* entry whose `id` starts with `<thread>: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), {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 () => {});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@
|
||||
//! ```no_run
|
||||
//! # async fn demo() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! 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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::Mutex<()>> = 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
|
||||
|
||||
@@ -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<OsString>,
|
||||
}
|
||||
|
||||
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!({
|
||||
|
||||
@@ -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<OsString>,
|
||||
}
|
||||
|
||||
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!({
|
||||
|
||||
Reference in New Issue
Block a user