diff --git a/__tests__/e2e/chat-launch.e2e.ts b/__tests__/e2e/chat-launch.e2e.ts index f749df7..ade7807 100644 --- a/__tests__/e2e/chat-launch.e2e.ts +++ b/__tests__/e2e/chat-launch.e2e.ts @@ -54,7 +54,6 @@ describe('when the user starts chat after onboarding', () => { await navigateToPlugins(session); await session.sendKey(ENTER); - await session.waitForText('Plugin installed successfully'); // Skip connecting tools await session.waitForText('Connect Confidence tools?'); diff --git a/__tests__/e2e/happy-path.e2e.ts b/__tests__/e2e/happy-path.e2e.ts index 7208c57..202e147 100644 --- a/__tests__/e2e/happy-path.e2e.ts +++ b/__tests__/e2e/happy-path.e2e.ts @@ -25,7 +25,6 @@ describe('happy-path flow', () => { await session.waitForText('Teach your AI'); await session.waitForText('Which agent tool are you using?'); await session.sendKey(ENTER); - await session.waitForText('Plugin installed successfully'); // ConnectTools await session.waitForText('Connect your AI to Confidence'); diff --git a/__tests__/e2e/helpers/index.ts b/__tests__/e2e/helpers/index.ts index f0c1730..1bd84fd 100644 --- a/__tests__/e2e/helpers/index.ts +++ b/__tests__/e2e/helpers/index.ts @@ -13,6 +13,8 @@ export { ARROW_DOWN, ARROW_UP, ENTER, ESCAPE } from './keys.js'; export { AUTH_CALLBACK_PORT } from './constants.js'; export { CHAT_PROMPT_FILE, ONBOARDING_INVOCATION_FILE } from './mock-binaries.js'; +const DEFAULT_TIMEOUT = 30_000; + type ProjectType = 'react' | 'empty'; export function createSession({ @@ -60,16 +62,19 @@ export function createSession({ } export async function simulateAuthCallback(): Promise { - const maxAttempts = 50; - for (let i = 0; i < maxAttempts; i++) { + const deadline = Date.now() + DEFAULT_TIMEOUT; + let backoff = 50; + + while (Date.now() < deadline) { try { await fetch(`http://localhost:${AUTH_CALLBACK_PORT}/callback?code=test-auth-code`); return; } catch { - await new Promise((resolve) => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, backoff)); + backoff = Math.min(backoff * 2, 500); } } - throw new Error(`Auth callback server not ready after ${maxAttempts * 100}ms`); + throw new Error(`Auth callback server not ready after ${DEFAULT_TIMEOUT / 1000}s`); } export async function navigatePastWelcome(session: TerminalSession): Promise { @@ -90,24 +95,23 @@ export async function navigatePastAuth(session: TerminalSession): Promise export async function navigateToPlugins(session: TerminalSession): Promise { await navigatePastWelcome(session); await navigatePastAuth(session); - await session.waitForText('Which agent tool are you using?'); session.checkpoint(); + await session.waitForText('Which agent tool are you using?'); } export async function navigateToConnectTools(session: TerminalSession): Promise { await navigateToPlugins(session); + await session.waitForText('Skip (install manually later)'); + session.checkpoint(); await session.sendKey(ENTER); - await session.waitForText('Plugin installed successfully'); await session.waitForText('Connect Confidence tools?'); - session.checkpoint(); } export async function navigateToOnboarding(session: TerminalSession): Promise { await navigateToConnectTools(session); + session.checkpoint(); await session.sendKey(ENTER); - await session.waitForText('Connected successfully'); await session.waitForText('Start onboarding?'); - session.checkpoint(); } export type Invocation = { @@ -122,13 +126,10 @@ export async function selectIdeAndOnboard( ): Promise { await session.sendKeyRepeat(ARROW_DOWN, downPresses); await session.sendKey(ENTER); - await session.waitForText('Plugin installed successfully'); // ConnectTools may auto-advance when MCP servers are already registered globally - const matched = await session.waitForAnyTextOf([ - 'Start onboarding?', - 'Connect Confidence tools?', - ]); + const matched = await session.waitForText(['Start onboarding?', 'Connect Confidence tools?']); + if (matched === 'Connect Confidence tools?') { await session.sendKey(ENTER); await session.waitForText('Connected successfully'); @@ -136,7 +137,7 @@ export async function selectIdeAndOnboard( await session.waitForText('Start onboarding?'); await session.sendKey(ENTER); - await session.waitForText('onboarding complete', { timeout: 60_000 }); + await session.waitForText('onboarding complete'); } export function readInvocation(cwd: string): Invocation { diff --git a/__tests__/e2e/helpers/pty.ts b/__tests__/e2e/helpers/pty.ts index ab9a23e..eeeb3f2 100644 --- a/__tests__/e2e/helpers/pty.ts +++ b/__tests__/e2e/helpers/pty.ts @@ -105,37 +105,29 @@ export class TerminalSession { } async waitForText( - text: string, - { timeout = DEFAULT_TIMEOUT, interval = 100, sinceCheckpoint = true } = {}, - ): Promise { - const deadline = Date.now() + timeout; - - while (Date.now() < deadline) { - const haystack = sinceCheckpoint ? this.screenSinceCheckpoint : this.screen; - if (haystack.includes(text)) return; - await delay(interval); - } - - throw new Error( - `Timed out waiting for "${text}" after ${timeout}ms.\n\nLast output:\n${this.screen.slice(-2000)}`, - ); - } - - async waitForAnyTextOf( - texts: string[], - { timeout = DEFAULT_TIMEOUT, interval = 100, sinceCheckpoint = true } = {}, + text: string | string[], + { timeout = DEFAULT_TIMEOUT, sinceCheckpoint = true } = {}, ): Promise { + const targets = Array.isArray(text) ? text : [text]; const deadline = Date.now() + timeout; + let poll = 25; while (Date.now() < deadline) { const haystack = sinceCheckpoint ? this.screenSinceCheckpoint : this.screen; - const match = texts.find((t) => haystack.includes(t)); + const match = targets.find((t) => haystack.includes(t)); if (match) return match; - await delay(interval); + + await delay(poll); + poll = Math.min(poll * 2, 200); } + const label = + targets.length === 1 + ? `"${targets[0]}"` + : `any of [${targets.map((t) => `"${t}"`).join(', ')}]`; + throw new Error( - `Timed out waiting for any of [${texts.map((t) => `"${t}"`).join(', ')}] after ${timeout}ms.\n\nLast output:\n${this.screen.slice(-2000)}`, + `Timed out waiting for ${label} after ${timeout}ms.\n\nLast output:\n${this.screen.slice(-2000)}`, ); } diff --git a/__tests__/e2e/stale-mcp-auth.e2e.ts b/__tests__/e2e/stale-mcp-auth.e2e.ts index e666a31..ae68c0a 100644 --- a/__tests__/e2e/stale-mcp-auth.e2e.ts +++ b/__tests__/e2e/stale-mcp-auth.e2e.ts @@ -50,7 +50,6 @@ describe('when MCP config has expired auth tokens', () => { // InstallPlugins await session.waitForText('Which agent tool are you using?'); await session.sendKey(ENTER); - await session.waitForText('Plugin installed successfully'); // ConnectTools — should detect expired auth await session.waitForText('auth expired'); @@ -72,7 +71,6 @@ describe('when MCP config has expired auth tokens', () => { // InstallPlugins await session.waitForText('Which agent tool are you using?'); await session.sendKey(ENTER); - await session.waitForText('Plugin installed successfully'); // ConnectTools — skip instead of reconnecting await session.waitForText('Reconnect all tools'); diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts index 1ff35bc..b0bbc2e 100644 --- a/vitest.config.e2e.ts +++ b/vitest.config.e2e.ts @@ -18,7 +18,7 @@ export default defineConfig({ globalSetup: ['__tests__/e2e/global-setup.ts'], setupFiles: [], testTimeout: 120_000, - hookTimeout: 30_000, + hookTimeout: 120_000, maxWorkers: 1, pool: 'forks', },