Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 suites/playwright-chat-app/suite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ workdir: /opt/app/data
required_env:
- CODEX_INIT_IMAGE
select: |
suite_tags=("svc_chat_app" "svc_gateway" "svc_agents_orchestrator" "svc_organizations" "svc_files" "svc_media_proxy")
suite_tags=("svc_chat_app" "svc_gateway" "svc_agents_orchestrator" "svc_organizations" "svc_files" "svc_media_proxy" "svc_tracing_app")
if [ -z "${TAGS:-}" ]; then
exit 0
Expand Down
8 changes: 8 additions & 0 deletions suites/playwright-chat-app/test/e2e/chat-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ type CreateAgentOptions = {
type SetupTestAgentOptions = {
endpoint: string;
initImage?: string;
protocol?: string;
remoteName?: string;
token?: string;
authMethod?: string;
};

type CreateTestModelOptions = {
Expand Down Expand Up @@ -473,6 +477,10 @@ export async function setupTestAgent(
organizationId,
endpoint: opts.endpoint,
namePrefix: 'e2e-model',
protocol: opts.protocol,
remoteName: opts.remoteName,
token: opts.token,
authMethod: opts.authMethod,
});

const agentName = `e2e-codex-agent-${now}`;
Expand Down
155 changes: 155 additions & 0 deletions suites/playwright-chat-app/test/e2e/chat-trace-link.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { randomUUID } from 'node:crypto';
import { expect, type Page } from '@playwright/test';
import { test } from './fixtures';
import {
createChat,
getMessages,
resolveIdentityId,
sendChatMessage,
setupTestAgent,
waitForAgentReply,
} from './chat-api';
import { setSelectedOrganization } from './organization-helpers';
import { completeOidcLogin } from './sign-in-helper';

const CODEX_TEST_LLM_ENDPOINT =
process.env.E2E_TEST_LLM_ENDPOINT ?? 'https://testllm.dev/v1/org/agynio/suite/codex/responses';
const CLAUDE_TEST_LLM_ENDPOINT =
process.env.E2E_TEST_LLM_ENDPOINT_CLAUDE ?? 'https://testllm.dev/v1/org/agynio/suite/claude/messages';
const CLAUDE_INIT_IMAGE = process.env.CLAUDE_INIT_IMAGE ?? 'ghcr.io/agynio/agent-init-claude:latest';
const CLAUDE_PROTOCOL = 'PROTOCOL_ANTHROPIC_MESSAGES';

function isTimeoutError(error: unknown): error is Error {
return error instanceof Error && error.name === 'TimeoutError';
}

type TraceScenario = {
name: string;
endpoint: string;
protocol?: string;
initImage?: string;
};

const TRACE_SCENARIOS: TraceScenario[] = [
{
name: 'codex',
endpoint: CODEX_TEST_LLM_ENDPOINT,
initImage: process.env.E2E_AGENT_INIT_IMAGE,
},
{
name: 'claude',
endpoint: CLAUDE_TEST_LLM_ENDPOINT,
protocol: CLAUDE_PROTOCOL,
initImage: CLAUDE_INIT_IMAGE,
},
];

async function openTraceFromChat(
page: Page,
params: { chatId: string; organizationId: string; messageId: string; messageText: string },
): Promise<void> {
const chatLoaded = page.waitForResponse(
(resp) => resp.url().includes('GetMessages') && resp.status() === 200,
{ timeout: 15000 },
);
await page.goto(`/chats/${encodeURIComponent(params.chatId)}`);
await chatLoaded;

const messageRow = page.getByTestId('chat-message').filter({ hasText: params.messageText }).first();
await expect(messageRow).toBeVisible({ timeout: 60000 });
await messageRow.hover();

const actionsTrigger = messageRow.getByTestId('message-actions-trigger');
await expect(actionsTrigger).toBeVisible({ timeout: 10000 });
await actionsTrigger.click();

const traceLink = page.getByTestId('message-trace-link');
await expect(traceLink).toBeVisible({ timeout: 10000 });

const traceHref = await traceLink.getAttribute('href');
if (!traceHref) {
throw new Error('Trace link is missing href.');
}

const traceUrl = new URL(traceHref, page.url());
expect(traceUrl.pathname).toBe(`/message/${params.messageId}`);
expect(traceUrl.searchParams.get('orgId')).toBe(params.organizationId);

const [tracePage] = await Promise.all([
page.waitForEvent('popup'),
traceLink.click(),
]);

await tracePage.waitForLoadState('domcontentloaded');

const callbackPromise = tracePage.waitForURL(/\/callback/, { timeout: 60000 }).catch((error) => {
if (isTimeoutError(error)) {
return null;
}
throw error;
});
const completed = await completeOidcLogin(tracePage, { timeoutMs: 10000 });
if (completed) {
await callbackPromise;
}

const runUrlPattern = new RegExp(`/${params.organizationId}/runs/[0-9a-f]{32}(\\?.*)?$`);
await expect(tracePage).toHaveURL(runUrlPattern, { timeout: 120000 });

await expect(tracePage.getByTestId('run-summary-status')).toContainText(/finished/i, { timeout: 120000 });

const eventsList = tracePage.getByTestId('run-events-list');
await expect(eventsList).toBeVisible({ timeout: 120000 });
const eventItems = eventsList.locator('[data-testid^="run-event-"]');
await expect.poll(() => eventItems.count(), { timeout: 120000 }).toBeGreaterThanOrEqual(2);

const messageEvent = eventsList.getByRole('button', { name: /Message • Source/ }).first();
await messageEvent.click();
await expect(tracePage.getByTestId('run-event-details-message-content')).toContainText(params.messageText);

const llmEvents = eventsList.getByRole('button', { name: /LLM Call/ });
await expect.poll(() => llmEvents.count(), { timeout: 120000 }).toBeGreaterThan(0);
await llmEvents.first().click();
await expect(tracePage.getByTestId('run-event-details-llm-output')).not.toHaveText('', { timeout: 120000 });

await tracePage.close();
}

test.describe('chat trace link', {
Comment thread
noa-lucent marked this conversation as resolved.
tag: ['@svc_chat_app', '@svc_tracing_app', '@svc_agents_orchestrator', '@svc_gateway', '@svc_organizations'],
}, () => {
for (const scenario of TRACE_SCENARIOS) {
test(`view trace opens tracing run (${scenario.name})`, async ({ page }) => {
test.setTimeout(8 * 60_000);

const { organizationId, participantId } = await setupTestAgent(page, {
endpoint: scenario.endpoint,
protocol: scenario.protocol,
initImage: scenario.initImage,
});
const identityId = await resolveIdentityId(page);
const chatId = await createChat(page, organizationId, participantId);
await setSelectedOrganization(page, organizationId);

const messageText = `trace-${scenario.name}-${randomUUID()}`;
await sendChatMessage(page, chatId, messageText);

await waitForAgentReply(page, chatId, identityId, 180_000);

const messages = await getMessages(page, chatId);
const userMessage = messages.find(
(message) => message.body === messageText && message.senderId === identityId,
);
if (!userMessage?.id) {
throw new Error(`Expected to find message id for ${messageText}.`);
}

await openTraceFromChat(page, {
chatId,
organizationId,
messageId: userMessage.id,
messageText,
});
});
}
});
4 changes: 2 additions & 2 deletions suites/playwright-chat-app/test/e2e/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Page } from '@playwright/test';
import { test as base, expect } from '@playwright/test';
import { signInViaMockAuth } from './sign-in-helper';
import { signInViaOidc } from './sign-in-helper';
export { expect };

async function signInAndLoad(page: Page) {
await signInViaMockAuth(page);
await signInViaOidc(page);
}

export const test = base.extend({
Expand Down
4 changes: 2 additions & 2 deletions suites/playwright-chat-app/test/e2e/multi-user-fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Browser, Page } from '@playwright/test';
import { test as base, expect } from '@playwright/test';
import { signInViaMockAuth } from './sign-in-helper';
import { signInViaOidc } from './sign-in-helper';

const USER_A_EMAIL = 'e2e-user-a@agyn.test';
const USER_B_EMAIL = 'e2e-user-b@agyn.test';
Expand All @@ -19,7 +19,7 @@ async function createUserContext(browser: Browser, email: string) {
page.on('requestfailed', (request) => {
console.log(`[request-failed] ${request.url()} — ${request.failure()?.errorText}`);
});
await signInViaMockAuth(page, email);
await signInViaOidc(page, email);
return { page, context };
}

Expand Down
Loading
Loading