Fix e2e daemon isolation and fast model defaults
This commit is contained in:
@@ -15,6 +15,11 @@ test('create agent in a temp repo', async ({ page }) => {
|
|||||||
// Verify user message is shown in the stream
|
// Verify user message is shown in the stream
|
||||||
await expect(page.getByText(prompt, { exact: true })).toBeVisible();
|
await expect(page.getByText(prompt, { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
// Verify we used a fast model (do not fall back to a default like Sonnet).
|
||||||
|
await page.getByTestId('agent-overflow-menu').click();
|
||||||
|
await expect(page.getByText('Model', { exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByText(/haiku/i)).toBeVisible();
|
||||||
|
|
||||||
// Wait for agent response containing "Hello" within an assistant message
|
// Wait for agent response containing "Hello" within an assistant message
|
||||||
const assistantMessage = page.getByTestId('assistant-message').filter({ hasText: 'Hello' });
|
const assistantMessage = page.getByTestId('assistant-message').filter({ hasText: 'Hello' });
|
||||||
await expect(assistantMessage).toBeVisible({ timeout: 30000 });
|
await expect(assistantMessage).toBeVisible({ timeout: 30000 });
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { test, expect } from './fixtures';
|
|||||||
import { gotoHome, openSettings } from './helpers/app';
|
import { gotoHome, openSettings } from './helpers/app';
|
||||||
|
|
||||||
test('daemon is connected in settings', async ({ page }) => {
|
test('daemon is connected in settings', async ({ page }) => {
|
||||||
const daemonPort = process.env.E2E_DAEMON_PORT ?? '6767';
|
const daemonPort = process.env.E2E_DAEMON_PORT;
|
||||||
|
if (!daemonPort) {
|
||||||
|
throw new Error('E2E_DAEMON_PORT is not set (expected from globalSetup).');
|
||||||
|
}
|
||||||
|
|
||||||
await gotoHome(page);
|
await gotoHome(page);
|
||||||
await openSettings(page);
|
await openSettings(page);
|
||||||
|
|
||||||
await expect(page.getByText('localhost', { exact: true })).toBeVisible();
|
|
||||||
await expect(page.getByText(`ws://localhost:${daemonPort}/ws`)).toBeVisible();
|
await expect(page.getByText(`ws://localhost:${daemonPort}/ws`)).toBeVisible();
|
||||||
await expect(page.getByText('Online', { exact: true })).toBeVisible();
|
await expect(page.getByText('Online', { exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import { test, expect, type Page } from '@playwright/test';
|
|||||||
const consoleEntries = new WeakMap<Page, string[]>();
|
const consoleEntries = new WeakMap<Page, string[]>();
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const daemonPort = process.env.E2E_DAEMON_PORT;
|
||||||
|
if (!daemonPort) {
|
||||||
|
throw new Error(
|
||||||
|
'E2E_DAEMON_PORT is not set. Refusing to run e2e against the default daemon (e.g. localhost:6767). ' +
|
||||||
|
'Ensure Playwright `globalSetup` starts the e2e daemon and exports E2E_DAEMON_PORT.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const entries: string[] = [];
|
const entries: string[] = [];
|
||||||
consoleEntries.set(page, entries);
|
consoleEntries.set(page, entries);
|
||||||
|
|
||||||
@@ -14,20 +22,36 @@ test.beforeEach(async ({ page }) => {
|
|||||||
entries.push(`[pageerror] ${error.message}`);
|
entries.push(`[pageerror] ${error.message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up test daemon connection if available
|
const nowIso = new Date().toISOString();
|
||||||
const daemonPort = process.env.E2E_DAEMON_PORT;
|
const testDaemon = {
|
||||||
if (daemonPort) {
|
id: 'e2e-test-daemon',
|
||||||
const testDaemon = {
|
label: 'localhost',
|
||||||
id: 'e2e-test-daemon',
|
wsUrl: `ws://localhost:${daemonPort}/ws`,
|
||||||
label: 'localhost',
|
restUrl: `http://localhost:${daemonPort}/`,
|
||||||
wsUrl: `ws://localhost:${daemonPort}/ws`,
|
createdAt: nowIso,
|
||||||
createdAt: new Date().toISOString(),
|
updatedAt: nowIso,
|
||||||
updatedAt: new Date().toISOString(),
|
};
|
||||||
};
|
|
||||||
await page.addInitScript((daemon) => {
|
const createAgentPreferences = {
|
||||||
|
// Ensure create flow never uses a remembered host from the developer's real app.
|
||||||
|
serverId: testDaemon.id,
|
||||||
|
// Keep e2e fast/cheap by default.
|
||||||
|
provider: 'claude',
|
||||||
|
providerPreferences: {
|
||||||
|
claude: { model: 'haiku' },
|
||||||
|
codex: { model: 'gpt-5.1-codex-mini' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.addInitScript(
|
||||||
|
({ daemon, preferences }) => {
|
||||||
|
// Hard-reset anything that could point to a developer's real daemon.
|
||||||
localStorage.setItem('@paseo:daemon-registry', JSON.stringify([daemon]));
|
localStorage.setItem('@paseo:daemon-registry', JSON.stringify([daemon]));
|
||||||
}, testDaemon);
|
localStorage.removeItem('@paseo:settings');
|
||||||
}
|
localStorage.setItem('@paseo:create-agent-preferences', JSON.stringify(preferences));
|
||||||
|
},
|
||||||
|
{ daemon: testDaemon, preferences: createAgentPreferences }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach(async ({ page }, testInfo) => {
|
test.afterEach(async ({ page }, testInfo) => {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { test, expect } from './fixtures';
|
||||||
|
import { createAgent, ensureHostSelected, gotoHome, setWorkingDirectory } from './helpers/app';
|
||||||
|
import { createTempGitRepo } from './helpers/workspace';
|
||||||
|
|
||||||
|
test('sidebar New Agent clones the selected agent settings (not last created)', async ({ page }) => {
|
||||||
|
const repoA = await createTempGitRepo();
|
||||||
|
const repoB = await createTempGitRepo();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gotoHome(page);
|
||||||
|
await ensureHostSelected(page);
|
||||||
|
|
||||||
|
await setWorkingDirectory(page, repoA.path);
|
||||||
|
await createAgent(page, 'Agent A: respond with exactly A');
|
||||||
|
await expect(page).toHaveURL(/\/agent\//);
|
||||||
|
const urlA = new URL(page.url());
|
||||||
|
const agentPathA = urlA.pathname;
|
||||||
|
|
||||||
|
await gotoHome(page);
|
||||||
|
await ensureHostSelected(page);
|
||||||
|
await setWorkingDirectory(page, repoB.path);
|
||||||
|
await createAgent(page, 'Agent B: respond with exactly B');
|
||||||
|
await expect(page).toHaveURL(/\/agent\//);
|
||||||
|
const urlB = new URL(page.url());
|
||||||
|
const agentPathB = urlB.pathname;
|
||||||
|
|
||||||
|
expect(agentPathA).not.toEqual(agentPathB);
|
||||||
|
|
||||||
|
// Navigate back to agent A via URL to ensure it's the selected agent.
|
||||||
|
await page.goto(agentPathA);
|
||||||
|
await expect(page).toHaveURL(agentPathA);
|
||||||
|
await expect(page.getByText('Agent A: respond with exactly A', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
// Click sidebar New Agent and assert it clones agent A's directory (not agent B's).
|
||||||
|
await page.getByTestId('sidebar-new-agent').click();
|
||||||
|
await expect(page).toHaveURL(new RegExp(encodeURIComponent(repoA.path)));
|
||||||
|
await expect(page).not.toHaveURL(new RegExp(encodeURIComponent(repoB.path)));
|
||||||
|
await expect(page.getByText(repoA.path, { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
// Now navigate to agent B and assert cloning uses repo B.
|
||||||
|
await page.goto(agentPathB);
|
||||||
|
await expect(page).toHaveURL(agentPathB);
|
||||||
|
await expect(page.getByText('Agent B: respond with exactly B', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId('sidebar-new-agent').click();
|
||||||
|
await expect(page).toHaveURL(new RegExp(encodeURIComponent(repoB.path)));
|
||||||
|
await expect(page).not.toHaveURL(new RegExp(encodeURIComponent(repoA.path)));
|
||||||
|
await expect(page.getByText(repoB.path, { exact: true })).toBeVisible();
|
||||||
|
} finally {
|
||||||
|
await repoA.cleanup();
|
||||||
|
await repoB.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
|
|||||||
// useLocalSearchParams doesn't update when navigating between same-pattern routes
|
// useLocalSearchParams doesn't update when navigating between same-pattern routes
|
||||||
const selectedAgentKey = useMemo(() => {
|
const selectedAgentKey = useMemo(() => {
|
||||||
// Match /agent/[serverId]/[agentId] pattern
|
// Match /agent/[serverId]/[agentId] pattern
|
||||||
const match = pathname.match(/^\/agent\/([^/]+)\/([^/]+)$/);
|
const match = pathname.match(/^\/agent\/([^/]+)\/([^/]+)\/?$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const [, serverId, agentId] = match;
|
const [, serverId, agentId] = match;
|
||||||
return `${serverId}:${agentId}`;
|
return `${serverId}:${agentId}`;
|
||||||
|
|||||||
@@ -624,7 +624,7 @@ function AgentScreenContent({
|
|||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<View ref={menuButtonRef} collapsable={false}>
|
<View ref={menuButtonRef} collapsable={false}>
|
||||||
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
|
<Pressable testID="agent-overflow-menu" onPress={handleOpenMenu} style={styles.menuButton}>
|
||||||
<MoreVertical size={16} color={theme.colors.foregroundMuted} />
|
<MoreVertical size={16} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyl
|
|||||||
import { Plus, Settings } from "lucide-react-native";
|
import { Plus, Settings } from "lucide-react-native";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { AgentList } from "./agent-list";
|
import { AgentList } from "./agent-list";
|
||||||
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
|
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
|
||||||
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
||||||
@@ -24,6 +25,30 @@ interface SlidingSidebarProps {
|
|||||||
selectedAgentId?: string;
|
selectedAgentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildCreateAgentRouteFromSelectedAgentKey(selectedAgentKey?: string) {
|
||||||
|
if (!selectedAgentKey) {
|
||||||
|
return { pathname: "/" as const, params: {} as Record<string, string> };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [serverId, agentId] = selectedAgentKey.split(":", 2);
|
||||||
|
if (!serverId || !agentId) {
|
||||||
|
return { pathname: "/" as const, params: {} as Record<string, string> };
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = useSessionStore.getState();
|
||||||
|
const agent = state.sessions[serverId]?.agents.get(agentId);
|
||||||
|
if (!agent) {
|
||||||
|
return { pathname: "/" as const, params: {} as Record<string, string> };
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: Record<string, string> = { serverId: agent.serverId };
|
||||||
|
if (agent.provider) params.provider = agent.provider;
|
||||||
|
if (agent.currentModeId) params.modeId = agent.currentModeId;
|
||||||
|
if (agent.model) params.model = agent.model;
|
||||||
|
if (agent.cwd) params.workingDir = agent.cwd;
|
||||||
|
return { pathname: "/" as const, params };
|
||||||
|
}
|
||||||
|
|
||||||
export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -81,17 +106,20 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
closeToAgent();
|
closeToAgent();
|
||||||
}, [closeToAgent]);
|
}, [closeToAgent]);
|
||||||
|
|
||||||
|
const handleCreateAgent = useCallback(() => {
|
||||||
|
router.push(buildCreateAgentRouteFromSelectedAgentKey(selectedAgentId));
|
||||||
|
}, [selectedAgentId]);
|
||||||
|
|
||||||
// Mobile: close sidebar and navigate
|
// Mobile: close sidebar and navigate
|
||||||
const handleCreateAgentMobile = useCallback(() => {
|
const handleCreateAgentMobile = useCallback(() => {
|
||||||
closeToAgent();
|
closeToAgent();
|
||||||
router.push("/");
|
handleCreateAgent();
|
||||||
}, [closeToAgent]);
|
}, [closeToAgent, handleCreateAgent]);
|
||||||
|
|
||||||
// Desktop: just navigate, don't close
|
// Desktop: just navigate, don't close
|
||||||
const handleCreateAgentDesktop = useCallback(() => {
|
const handleCreateAgentDesktop = useCallback(() => {
|
||||||
router.push("/");
|
handleCreateAgent();
|
||||||
}, []);
|
}, [handleCreateAgent]);
|
||||||
|
|
||||||
// Mobile: close sidebar and navigate
|
// Mobile: close sidebar and navigate
|
||||||
const handleSettingsMobile = useCallback(() => {
|
const handleSettingsMobile = useCallback(() => {
|
||||||
@@ -211,6 +239,7 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
styles.newAgentButton,
|
styles.newAgentButton,
|
||||||
hovered && styles.newAgentButtonHovered,
|
hovered && styles.newAgentButtonHovered,
|
||||||
]}
|
]}
|
||||||
|
testID="sidebar-new-agent"
|
||||||
onPress={handleCreateAgentMobile}
|
onPress={handleCreateAgentMobile}
|
||||||
>
|
>
|
||||||
<Plus size={18} color={theme.colors.foreground} />
|
<Plus size={18} color={theme.colors.foreground} />
|
||||||
@@ -262,6 +291,7 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
styles.newAgentButton,
|
styles.newAgentButton,
|
||||||
hovered && styles.newAgentButtonHovered,
|
hovered && styles.newAgentButtonHovered,
|
||||||
]}
|
]}
|
||||||
|
testID="sidebar-new-agent"
|
||||||
onPress={handleCreateAgentDesktop}
|
onPress={handleCreateAgentDesktop}
|
||||||
>
|
>
|
||||||
<Plus size={18} color={theme.colors.foreground} />
|
<Plus size={18} color={theme.colors.foreground} />
|
||||||
|
|||||||
@@ -163,8 +163,14 @@ function resolveFormState(
|
|||||||
} else {
|
} else {
|
||||||
result.model = "";
|
result.model = "";
|
||||||
}
|
}
|
||||||
} else if (providerPrefs?.model && isValidModel(providerPrefs.model)) {
|
} else if (typeof providerPrefs?.model === "string" && providerPrefs.model.length > 0) {
|
||||||
result.model = providerPrefs.model;
|
// If models haven't loaded yet, optimistically apply the stored preference.
|
||||||
|
// We'll validate once models load and clear it if it isn't available.
|
||||||
|
if (!availableModels || isValidModel(providerPrefs.model)) {
|
||||||
|
result.model = providerPrefs.model;
|
||||||
|
} else {
|
||||||
|
result.model = "";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
result.model = "";
|
result.model = "";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user