mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-23 21:12:22 +02:00
83 lines
4.2 KiB
TypeScript
83 lines
4.2 KiB
TypeScript
import { test, expect, ElectronApplication, Page } from '@playwright/test';
|
|
import { launchApp, waitForMainWindow, hasAnyProviderKey } from '../helpers/launch';
|
|
import { startVisibility, VisibilityHandle } from '../helpers/visibility';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
|
|
// Real provider round-trip: types a tiny prompt into a new agent, sends it, and
|
|
// asserts an assistant message bubble arrives with non-empty text and no
|
|
// renderer crash. Auto-skips entirely when no provider key is in env, so CI
|
|
// legs without Actions Secrets stay green. Keys come from process.env only;
|
|
// nothing is read from or written to a committed file.
|
|
|
|
function backendLogPath(): string {
|
|
if (process.platform === 'win32') return path.join(process.env.APPDATA || '', 'OpenSwarm', 'data', 'backend.log');
|
|
if (process.platform === 'darwin') return path.join(os.homedir(), 'Library', 'Application Support', 'OpenSwarm', 'data', 'backend.log');
|
|
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'OpenSwarm', 'data', 'backend.log');
|
|
}
|
|
function crashCount(): number {
|
|
try { return (fs.readFileSync(backendLogPath(), 'utf8').match(/renderer process gone/g) || []).length; }
|
|
catch { return 0; }
|
|
}
|
|
|
|
test.describe.configure({ mode: 'serial' });
|
|
test.describe('real agent round-trip', () => {
|
|
let app: ElectronApplication;
|
|
let page: Page;
|
|
let baselineCrashes = 0;
|
|
let vis: VisibilityHandle;
|
|
|
|
// Whole describe skips with a clear reason when no key is wired, so we
|
|
// never silently green this on a leg that can't actually test it.
|
|
test.beforeAll(async () => {
|
|
test.skip(!hasAnyProviderKey(), 'no provider env key set; pass ANTHROPIC_API_KEY or OPENAI_API_KEY etc. to enable');
|
|
test.skip(process.env.CI !== 'true' && process.env.OPENSWARM_E2E_SEED !== '1', 'seed gate not enabled; set OPENSWARM_E2E_SEED=1 for local runs');
|
|
app = await launchApp();
|
|
page = await waitForMainWindow(app);
|
|
vis = await startVisibility(app, page, 'real-agent-roundtrip');
|
|
baselineCrashes = crashCount();
|
|
});
|
|
test.afterAll(async () => {
|
|
try { await vis?.stop(); } catch {}
|
|
await app?.close().catch(() => {});
|
|
});
|
|
|
|
test('compose, send, and receive an assistant reply', async ({}, info) => {
|
|
// Find the New Agent button on the dashboard toolbar.
|
|
const newAgentBtn = page.locator('[data-onboarding="new-agent-button"]');
|
|
await expect(newAgentBtn).toBeVisible({ timeout: 15_000 });
|
|
await newAgentBtn.click();
|
|
|
|
const editor = page.locator('[data-onboarding="chat-input"]').first();
|
|
await expect(editor, 'EditorSurface did not mount').toBeVisible({ timeout: 15_000 });
|
|
await editor.click();
|
|
await page.keyboard.type('reply with the single word: pong', { delay: 10 });
|
|
await expect.poll(async () => (await editor.innerText()).trim(), { timeout: 5_000 }).toContain('pong');
|
|
|
|
const sendBtn = page.locator('[data-onboarding="chat-send-button"]');
|
|
await expect(sendBtn, 'send button never enabled; provider likely unconfigured').toBeVisible({ timeout: 10_000 });
|
|
await sendBtn.click();
|
|
await page.screenshot({ path: info.outputPath('after-send.png') });
|
|
|
|
// Wait for an assistant bubble to appear with non-empty text. Bubbles tag
|
|
// themselves via data-select-meta JSON; matching on substring is enough.
|
|
const assistantBubble = page.locator('[data-select-type="message"][data-select-meta*="\\"role\\":\\"assistant\\""]');
|
|
await expect.poll(async () => assistantBubble.count(), { timeout: 120_000 }).toBeGreaterThan(0);
|
|
|
|
// Allow the streaming bubble a moment to accumulate text past zero chars.
|
|
await expect.poll(async () => {
|
|
const n = await assistantBubble.count();
|
|
if (n === 0) return 0;
|
|
const text = (await assistantBubble.first().innerText()).trim();
|
|
return text.length;
|
|
}, { timeout: 60_000 }).toBeGreaterThan(0);
|
|
|
|
const finalText = (await assistantBubble.first().innerText()).trim();
|
|
expect(finalText.length, 'assistant bubble appeared but text never populated').toBeGreaterThan(0);
|
|
await page.screenshot({ path: info.outputPath('assistant-replied.png') });
|
|
|
|
expect(crashCount(), 'renderer crashed during real round-trip').toBe(baselineCrashes);
|
|
});
|
|
});
|