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
1 change: 0 additions & 1 deletion __tests__/e2e/chat-launch.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?');
Expand Down
1 change: 0 additions & 1 deletion __tests__/e2e/happy-path.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
31 changes: 16 additions & 15 deletions __tests__/e2e/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -60,16 +62,19 @@ export function createSession({
}

export async function simulateAuthCallback(): Promise<void> {
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<void> {
Expand All @@ -90,24 +95,23 @@ export async function navigatePastAuth(session: TerminalSession): Promise<void>
export async function navigateToPlugins(session: TerminalSession): Promise<void> {
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<void> {
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<void> {
await navigateToConnectTools(session);
session.checkpoint();
await session.sendKey(ENTER);
await session.waitForText('Connected successfully');
await session.waitForText('Start onboarding?');
session.checkpoint();
}

export type Invocation = {
Expand All @@ -122,21 +126,18 @@ export async function selectIdeAndOnboard(
): Promise<void> {
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');
}

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 {
Expand Down
36 changes: 14 additions & 22 deletions __tests__/e2e/helpers/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,37 +105,29 @@ export class TerminalSession {
}

async waitForText(
text: string,
{ timeout = DEFAULT_TIMEOUT, interval = 100, sinceCheckpoint = true } = {},
): Promise<void> {
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<string> {
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)}`,
);
}

Expand Down
2 changes: 0 additions & 2 deletions __tests__/e2e/stale-mcp-auth.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion vitest.config.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down