Merge branch 'fix-tests-suite-loop'

# Conflicts:
#	package-lock.json
#	packages/server/src/server/speech/speech-config-resolver.ts
#	packages/server/src/shared/tool-call-display.ts
This commit is contained in:
Mohamed Boudra
2026-03-14 22:06:42 +07:00
50 changed files with 1330 additions and 2544 deletions
+26 -13
View File
@@ -18,7 +18,20 @@ test.describe.configure({ timeout: 180000 });
async function openWorkspaceAgentTab(page: Page, agentId: string) {
const tab = page.getByTestId(`workspace-tab-agent_${agentId}`).first();
await expect(tab).toBeVisible({ timeout: 30000 });
await tab.click();
const trigger = tab.getByRole("button").first();
const clickableTarget = (await trigger.count()) > 0 ? trigger : tab;
await clickableTarget.scrollIntoViewIfNeeded();
await clickableTarget.click({ timeout: 5000 });
}
function buildWorkspaceDraftUrl(workspaceUrl: string) {
return `${workspaceUrl}?open=${encodeURIComponent("draft:new")}`;
}
async function expectChatContainerKey(page: Page, expectedKey: string) {
await expect
.poll(async () => await getChatContainerKey(page))
.toBe(expectedKey);
}
test("direct load and refresh land at the bottom for history-backed chats", async ({
@@ -69,7 +82,9 @@ test("revisiting a loaded chat restores bottom anchoring", async ({
await waitForAgentReady(page, agent.expectedTailText);
await expectNearBottom(page);
await page.getByTestId("sidebar-new-agent").first().click();
await page.goto(buildWorkspaceDraftUrl(agent.workspaceUrl), {
waitUntil: "domcontentloaded",
});
await expect(page.getByRole("textbox", { name: "Message agent..." }).first()).toBeVisible({
timeout: 30000,
});
@@ -95,6 +110,7 @@ test("sticky mode stays pinned through composer growth and viewport resize, but
cwd: repo.path,
title: `bottom-anchor-sticky-${Date.now()}`,
turnCount: 10,
lineCount: 24,
});
await page.setViewportSize({ width: 1320, height: 920 });
@@ -114,7 +130,11 @@ test("sticky mode stays pinned through composer growth and viewport resize, but
await expectNearBottom(page);
await expect(page.getByTestId("scroll-to-bottom-button")).toHaveCount(0);
await page.setViewportSize({ width: 820, height: 760 });
await page.setViewportSize({ width: 1040, height: 760 });
await expect(page.getByTestId(`workspace-tab-agent_${agent.id}`).first()).toBeVisible({
timeout: 30000,
});
await waitForAgentReady(page, agent.expectedTailText);
await expectNearBottom(page);
await scrollUpFromBottom(page, 720);
@@ -133,7 +153,7 @@ test("sticky mode stays pinned through composer growth and viewport resize, but
}
});
test("web partial virtualization keeps bottom anchoring stable across direct load, refresh, and resize", async ({
test("web DOM virtualization keeps bottom anchoring stable across direct load and refresh", async ({
page,
}) => {
await page.addInitScript(() => {
@@ -161,20 +181,13 @@ test("web partial virtualization keeps bottom anchoring stable across direct loa
await page.goto(agent.url, { waitUntil: "domcontentloaded" });
await openWorkspaceAgentTab(page, agent.id);
await waitForAgentReady(page, agent.expectedTailText);
await expect
.poll(async () => await getChatContainerKey(page))
.toBe("web-partial-virtualized");
await expectChatContainerKey(page, "web-dom-virtualized");
await expectNearBottom(page);
await page.reload({ waitUntil: "commit" });
await openWorkspaceAgentTab(page, agent.id);
await waitForAgentReady(page, agent.expectedTailText);
await expect
.poll(async () => await getChatContainerKey(page))
.toBe("web-partial-virtualized");
await expectNearBottom(page);
await page.setViewportSize({ width: 780, height: 720 });
await expectChatContainerKey(page, "web-dom-virtualized");
await expectNearBottom(page);
} finally {
await client.close().catch(() => undefined);
@@ -1,4 +1,8 @@
import { expect, test, type Page } from "@playwright/test";
import {
buildCreateAgentPreferences,
buildSeededHost,
} from "./helpers/daemon-registry";
const SERVER_ID =
process.env.PLAYWRIGHT_REPRO_SERVER_ID ?? "srv_ETXtcjYRGrCI";
@@ -26,33 +30,17 @@ function seedDaemonRegistryScript(params: {
endpoint: string;
nowIso: string;
}) {
const daemon = {
const daemon = buildSeededHost({
serverId: params.serverId,
label: "localhost",
connections: [
{
id: `direct:${params.endpoint}`,
type: "direct",
endpoint: params.endpoint,
},
],
preferredConnectionId: `direct:${params.endpoint}`,
createdAt: params.nowIso,
updatedAt: params.nowIso,
};
endpoint: params.endpoint,
nowIso: params.nowIso,
});
localStorage.setItem("@paseo:e2e", "1");
localStorage.setItem("@paseo:daemon-registry", JSON.stringify([daemon]));
localStorage.setItem(
"@paseo:create-agent-preferences",
JSON.stringify({
serverId: params.serverId,
provider: "codex",
providerPreferences: {
claude: { model: "haiku" },
codex: { model: "gpt-5.1-codex-mini", thinkingOptionId: "low" },
},
})
JSON.stringify(buildCreateAgentPreferences(params.serverId))
);
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { test, expect } from './fixtures';
import { gotoHome, openSettings } from './helpers/app';
import { gotoAppShell, openSettings } from './helpers/app';
test('daemon is connected in settings', async ({ page }) => {
const daemonPort = process.env.E2E_DAEMON_PORT;
@@ -11,7 +11,7 @@ test('daemon is connected in settings', async ({ page }) => {
throw new Error('E2E_SERVER_ID is not set (expected from globalSetup).');
}
await gotoHome(page);
await gotoAppShell(page);
await openSettings(page);
await expect(page.getByText(`127.0.0.1:${daemonPort}`)).toBeVisible();
+9 -24
View File
@@ -1,4 +1,8 @@
import { test as base, expect, type Page } from '@playwright/test';
import {
buildCreateAgentPreferences,
buildSeededHost,
} from './helpers/daemon-registry';
// Extend base test to provide dynamic baseURL from global-setup
const test = base.extend({
@@ -58,31 +62,12 @@ test.beforeEach(async ({ page }) => {
if (!serverId) {
throw new Error('E2E_SERVER_ID is not set - expected from Playwright globalSetup.');
}
const testDaemon = {
const testDaemon = buildSeededHost({
serverId,
label: 'localhost',
connections: [
{
id: `direct:127.0.0.1:${daemonPort}`,
type: 'direct',
endpoint: `127.0.0.1:${daemonPort}`,
},
],
preferredConnectionId: `direct:127.0.0.1:${daemonPort}`,
createdAt: nowIso,
updatedAt: nowIso,
};
const createAgentPreferences = {
// Ensure create flow never uses a remembered host from the developer's real app.
serverId: testDaemon.serverId,
// Keep e2e fast/cheap by default.
provider: 'codex',
providerPreferences: {
claude: { model: 'haiku' },
codex: { model: 'gpt-5.1-codex-mini', thinkingOptionId: 'low' },
},
};
endpoint: `127.0.0.1:${daemonPort}`,
nowIso,
});
const createAgentPreferences = buildCreateAgentPreferences(testDaemon.serverId);
await page.addInitScript(
({ daemon, preferences, seedNonce }) => {
+14
View File
@@ -199,6 +199,19 @@ function stripAnsi(input: string): string {
return input.replace(/\u001b\[[0-9;]*m/g, '');
}
function ensureRelayBuildArtifact(repoRoot: string): void {
const relayDistEntry = path.join(repoRoot, 'packages/relay/dist/e2ee.js');
if (existsSync(relayDistEntry)) {
return;
}
console.log('[e2e] Building @getpaseo/relay for daemon startup');
execSync('npm run build --workspace=@getpaseo/relay', {
cwd: repoRoot,
stdio: 'inherit',
});
}
function decodeOfferFromFragmentUrl(url: string): OfferPayload {
const marker = '#offer=';
const idx = url.indexOf(marker);
@@ -217,6 +230,7 @@ function decodeOfferFromFragmentUrl(url: string): OfferPayload {
export default async function globalSetup() {
const repoRoot = path.resolve(__dirname, '../../..');
ensureRelayBuildArtifact(repoRoot);
const envTestPath = path.join(repoRoot, '.env.test');
if (existsSync(envTestPath)) {
dotenv.config({ path: envTestPath });
+55 -38
View File
@@ -66,17 +66,17 @@ function buildReplyBlock(label: string, lineCount = 14): string {
}).join("\n");
}
function buildProtocolMessage(label: string): string {
function buildProtocolMessage(label: string, lineCount = 14): string {
return [
"For every message in this chat, reply with exactly the text after the final line `REPLY:`.",
"Do not add extra words, bullets, markdown fences, or tool calls.",
"REPLY:",
buildReplyBlock(label),
buildReplyBlock(label, lineCount),
].join("\n");
}
function buildReplyMessage(label: string): string {
return ["REPLY:", buildReplyBlock(label)].join("\n");
function buildReplyMessage(label: string, lineCount = 14): string {
return ["REPLY:", buildReplyBlock(label, lineCount)].join("\n");
}
export function createReplyTurn(label: string): {
@@ -124,9 +124,11 @@ export async function seedBottomAnchorAgent(input: {
cwd: string;
title?: string;
turnCount?: number;
lineCount?: number;
}): Promise<SeededAgent> {
const title = input.title ?? `bottom-anchor-${Date.now()}`;
const turnCount = Math.max(3, input.turnCount ?? 5);
const lineCount = Math.max(14, input.lineCount ?? 14);
const created = await input.client.createAgent({
provider: "codex",
model: "gpt-5.1-codex-mini",
@@ -134,7 +136,7 @@ export async function seedBottomAnchorAgent(input: {
modeId: "full-access",
cwd: input.cwd,
title,
initialPrompt: buildProtocolMessage(`${title}-turn-00`),
initialPrompt: buildProtocolMessage(`${title}-turn-00`, lineCount),
});
const initialFinish = await input.client.waitForFinish(created.id, 120000);
if (initialFinish.status !== "idle") {
@@ -143,11 +145,11 @@ export async function seedBottomAnchorAgent(input: {
);
}
let expectedTailText = buildReplyBlock(`${title}-turn-00`);
let expectedTailText = buildReplyBlock(`${title}-turn-00`, lineCount);
for (let index = 1; index < turnCount; index += 1) {
const label = `${title}-turn-${index.toString().padStart(2, "0")}`;
expectedTailText = buildReplyBlock(label);
await input.client.sendAgentMessage(created.id, buildReplyMessage(label));
expectedTailText = buildReplyBlock(label, lineCount);
await input.client.sendAgentMessage(created.id, buildReplyMessage(label, lineCount));
const finish = await input.client.waitForFinish(created.id, 120000);
if (finish.status !== "idle") {
throw new Error(
@@ -165,16 +167,29 @@ export async function seedBottomAnchorAgent(input: {
};
}
function getVisibleChatScroll(page: Page) {
return page.locator('[data-testid="agent-chat-scroll"]:visible').first();
}
export async function readScrollMetrics(page: Page): Promise<ScrollMetrics> {
return page.getByTestId("agent-chat-scroll").evaluate((root: Element) => {
const rootElement = root as HTMLElement;
const candidates = [rootElement, ...Array.from(rootElement.querySelectorAll("*"))];
return getVisibleChatScroll(page).evaluate((root: Element) => {
const candidates = [root, ...Array.from(root.querySelectorAll("*"))]
.filter((element): element is HTMLElement => element instanceof HTMLElement)
.filter((element) => {
const tagName = element.tagName.toLowerCase();
const isEditable =
tagName === "textarea" ||
tagName === "input" ||
element.getAttribute("contenteditable") === "true";
return !isEditable && element.scrollHeight - element.clientHeight > 1;
});
const scrollElement =
candidates.find(
(element) =>
element instanceof HTMLElement &&
element.scrollHeight - element.clientHeight > 1
) ?? rootElement;
candidates.sort(
(left, right) =>
right.scrollHeight -
right.clientHeight -
(left.scrollHeight - left.clientHeight)
)[0] ?? (root as HTMLElement);
const offsetY = Math.max(0, scrollElement.scrollTop);
const contentHeight = Math.max(0, scrollElement.scrollHeight);
@@ -194,29 +209,33 @@ export async function readScrollMetrics(page: Page): Promise<ScrollMetrics> {
}
export async function scrollUpFromBottom(page: Page, pixels: number): Promise<void> {
await page.getByTestId("agent-chat-scroll").evaluate(
(root: Element, amount: number) => {
const rootElement = root as HTMLElement;
const candidates = [rootElement, ...Array.from(rootElement.querySelectorAll("*"))];
const scrollElement =
candidates.find(
(element) =>
element instanceof HTMLElement &&
element.scrollHeight - element.clientHeight > 1
) ?? rootElement;
const bottomOffset = Math.max(
0,
scrollElement.scrollHeight - scrollElement.clientHeight
const scrollViewport = getVisibleChatScroll(page);
await expect(scrollViewport).toHaveCount(1, { timeout: 30000 });
let remaining = Math.max(0, pixels);
while (remaining > 0) {
const delta = Math.min(240, remaining);
await scrollViewport.evaluate((element: Element, step: number) => {
const scrollContainer = element as HTMLElement;
scrollContainer.dispatchEvent(
new WheelEvent("wheel", {
deltaY: -step,
bubbles: true,
cancelable: true,
})
);
scrollElement.scrollTop = Math.max(0, bottomOffset - amount);
},
pixels
);
scrollContainer.scrollTop = Math.max(0, scrollContainer.scrollTop - step);
scrollContainer.dispatchEvent(new Event("scroll", { bubbles: true }));
}, delta);
remaining -= delta;
if ((await readScrollMetrics(page)).distanceFromBottom > NEAR_BOTTOM_THRESHOLD_PX) {
return;
}
}
}
export async function waitForAgentReady(page: Page, expectedTailText?: string): Promise<void> {
await expect(page.getByTestId("agent-chat-scroll")).toBeVisible({ timeout: 60000 });
await expect(getVisibleChatScroll(page)).toBeVisible({ timeout: 60000 });
await expect(page.getByRole("textbox", { name: "Message agent..." }).first()).toBeVisible({
timeout: 60000,
});
@@ -263,9 +282,7 @@ export async function waitForContentGrowth(
}
export async function getChatContainerKey(page: Page): Promise<string | null> {
return page
.getByTestId("agent-chat-scroll")
.evaluate((element) => {
return getVisibleChatScroll(page).evaluate((element) => {
const nativeId = (element as HTMLElement).id;
const prefix = "agent-chat-scroll-";
return nativeId.startsWith(prefix) ? nativeId.slice(prefix.length) : null;
+44 -40
View File
@@ -1,4 +1,8 @@
import { expect, type Page } from '@playwright/test';
import {
buildCreateAgentPreferences,
buildSeededHost,
} from './daemon-registry';
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -33,8 +37,8 @@ async function ensureE2EStorageSeeded(page: Page): Promise<void> {
if (entry?.serverId !== expectedServerId) return true;
const connections = entry?.connections;
if (!Array.isArray(connections)) return true;
if (connections.some((c: any) => c?.type === 'direct' && typeof c?.endpoint === 'string' && /:6767\b/.test(c.endpoint))) return true;
return !connections.some((c: any) => c?.type === 'direct' && c?.endpoint === expectedEndpoint);
if (connections.some((c: any) => c?.type === 'directTcp' && typeof c?.endpoint === 'string' && /:6767\b/.test(c.endpoint))) return true;
return !connections.some((c: any) => c?.type === 'directTcp' && c?.endpoint === expectedEndpoint);
} catch {
return true;
}
@@ -45,42 +49,20 @@ async function ensureE2EStorageSeeded(page: Page): Promise<void> {
}
const nowIso = new Date().toISOString();
const daemon = buildSeededHost({
serverId: expectedServerId,
endpoint: expectedEndpoint,
nowIso,
});
const preferences = buildCreateAgentPreferences(expectedServerId);
await page.evaluate(
({ expectedEndpoint, nowIso, expectedServerId }) => {
({ daemon, preferences }) => {
localStorage.setItem('@paseo:e2e', '1');
localStorage.setItem(
'@paseo:daemon-registry',
JSON.stringify([
{
serverId: expectedServerId,
label: 'localhost',
connections: [
{
id: `direct:${expectedEndpoint}`,
type: 'direct',
endpoint: expectedEndpoint,
},
],
preferredConnectionId: `direct:${expectedEndpoint}`,
createdAt: nowIso,
updatedAt: nowIso,
},
])
);
localStorage.setItem(
'@paseo:create-agent-preferences',
JSON.stringify({
serverId: expectedServerId,
provider: 'codex',
providerPreferences: {
claude: { model: 'haiku' },
codex: { model: 'gpt-5.1-codex-mini', thinkingOptionId: 'low' },
},
})
);
localStorage.setItem('@paseo:daemon-registry', JSON.stringify([daemon]));
localStorage.setItem('@paseo:create-agent-preferences', JSON.stringify(preferences));
localStorage.removeItem('@paseo:settings');
},
{ expectedEndpoint, nowIso, expectedServerId }
{ daemon, preferences }
);
await page.reload();
@@ -128,13 +110,13 @@ async function assertE2EUsesSeededTestDaemon(page: Page): Promise<void> {
const connections: unknown = daemon?.connections;
if (
!Array.isArray(connections) ||
!connections.some((c: any) => c?.type === 'direct' && c?.endpoint === expectedEndpoint)
!connections.some((c: any) => c?.type === 'directTcp' && c?.endpoint === expectedEndpoint)
) {
throw new Error(
`E2E expected seeded daemon connections to include direct ${expectedEndpoint} (got ${JSON.stringify(connections)}).`
`E2E expected seeded daemon connections to include directTcp ${expectedEndpoint} (got ${JSON.stringify(connections)}).`
);
}
if (Array.isArray(connections) && connections.some((c: any) => c?.type === 'direct' && typeof c?.endpoint === 'string' && /:6767\b/.test(c.endpoint))) {
if (Array.isArray(connections) && connections.some((c: any) => c?.type === 'directTcp' && typeof c?.endpoint === 'string' && /:6767\b/.test(c.endpoint))) {
throw new Error(`E2E detected a daemon endpoint pointing at :6767 (${JSON.stringify(connections)}).`);
}
@@ -154,14 +136,36 @@ async function assertE2EUsesSeededTestDaemon(page: Page): Promise<void> {
}
}
export const gotoHome = async (page: Page) => {
export const gotoAppShell = async (page: Page) => {
await page.goto('/');
await ensureE2EStorageSeeded(page);
await expect(page.getByText('New agent', { exact: true }).first()).toBeVisible();
};
export const gotoHome = async (page: Page) => {
await gotoAppShell(page);
const composer = page.getByRole('textbox', { name: 'Message agent...' });
if (!(await composer.first().isVisible().catch(() => false))) {
const addProjectCta = page.getByText('Add a project', { exact: true }).first();
const addProjectSidebar = page.getByText('Add project', { exact: true }).first();
const newAgentButton = page.getByText('New agent', { exact: true }).first();
await newAgentButton.click();
await expect
.poll(
async () =>
(await addProjectCta.isVisible().catch(() => false)) ||
(await addProjectSidebar.isVisible().catch(() => false)) ||
(await newAgentButton.isVisible().catch(() => false)),
{ timeout: 10000 }
)
.toBe(true);
if (await addProjectCta.isVisible().catch(() => false)) {
await addProjectCta.click();
} else if (await addProjectSidebar.isVisible().catch(() => false)) {
await addProjectSidebar.click();
} else {
await newAgentButton.click();
}
}
await expect(composer.first()).toBeVisible({ timeout: 30000 });
};
@@ -0,0 +1,39 @@
export const TEST_HOST_LABEL = "localhost";
export const TEST_PROVIDER_PREFERENCES = {
claude: { model: "haiku" },
codex: { model: "gpt-5.1-codex-mini", thinkingOptionId: "low" },
} as const;
export function buildDirectTcpConnection(endpoint: string) {
return {
id: `direct:${endpoint}`,
type: "directTcp" as const,
endpoint,
};
}
export function buildSeededHost(input: {
serverId: string;
endpoint: string;
label?: string;
nowIso: string;
}) {
const connection = buildDirectTcpConnection(input.endpoint);
return {
serverId: input.serverId,
label: input.label ?? TEST_HOST_LABEL,
connections: [connection],
preferredConnectionId: connection.id,
createdAt: input.nowIso,
updatedAt: input.nowIso,
};
}
export function buildCreateAgentPreferences(serverId: string) {
return {
serverId,
provider: "codex" as const,
providerPreferences: TEST_PROVIDER_PREFERENCES,
};
}
+11 -17
View File
@@ -1,4 +1,7 @@
import { test, expect } from './fixtures';
import {
buildSeededHost,
} from './helpers/daemon-registry';
test('host removal removes the host from UI and persists after reload', async ({ page }) => {
const daemonPort = process.env.E2E_DAEMON_PORT;
@@ -14,27 +17,18 @@ test('host removal removes the host from UI and persists after reload', async ({
const extraEndpoint = `127.0.0.1:${extraPort}`;
const nowIso = new Date().toISOString();
const extraDaemon = {
const extraDaemon = buildSeededHost({
serverId: 'srv_e2e_extra_daemon',
endpoint: extraEndpoint,
label: 'extra',
connections: [
{ id: `direct:${extraEndpoint}`, type: 'direct', endpoint: extraEndpoint },
],
preferredConnectionId: `direct:${extraEndpoint}`,
createdAt: nowIso,
updatedAt: nowIso,
};
nowIso,
});
const seededTestDaemon = {
const seededTestDaemon = buildSeededHost({
serverId: seededServerId,
label: 'localhost',
connections: [
{ id: `direct:127.0.0.1:${daemonPort}`, type: 'direct', endpoint: `127.0.0.1:${daemonPort}` },
],
preferredConnectionId: `direct:127.0.0.1:${daemonPort}`,
createdAt: nowIso,
updatedAt: nowIso,
};
endpoint: `127.0.0.1:${daemonPort}`,
nowIso,
});
const seedOnceKey = `@paseo:e2e-host-removal-seeded:${Math.random().toString(36).slice(2)}`;
+17 -35
View File
@@ -1,4 +1,8 @@
import { test, expect } from './fixtures';
import {
buildCreateAgentPreferences,
buildSeededHost,
} from './helpers/daemon-registry';
import { ensureHostSelected, gotoHome } from './helpers/app';
test('new agent auto-selects the previous host', async ({ page }) => {
@@ -25,28 +29,12 @@ test('new agent respects serverId in the URL', async ({ page }) => {
// Ensure this test's storage is deterministic even under parallel load.
const nowIso = new Date().toISOString();
const testDaemon = {
const testDaemon = buildSeededHost({
serverId,
label: 'localhost',
connections: [
{
id: `direct:127.0.0.1:${daemonPort}`,
type: 'direct',
endpoint: `127.0.0.1:${daemonPort}`,
},
],
preferredConnectionId: `direct:127.0.0.1:${daemonPort}`,
createdAt: nowIso,
updatedAt: nowIso,
};
const createAgentPreferences = {
serverId: testDaemon.serverId,
provider: 'codex',
providerPreferences: {
claude: { model: 'haiku' },
codex: { model: 'gpt-5.1-codex-mini', thinkingOptionId: 'low' },
},
};
endpoint: `127.0.0.1:${daemonPort}`,
nowIso,
});
const createAgentPreferences = buildCreateAgentPreferences(testDaemon.serverId);
await page.goto('/settings');
await page.evaluate(
@@ -60,7 +48,10 @@ test('new agent respects serverId in the URL', async ({ page }) => {
{ daemon: testDaemon, preferences: createAgentPreferences }
);
await page.reload();
await expect(page.getByText('New agent', { exact: true }).first()).toBeVisible({ timeout: 20000 });
await expect(page.getByText('Settings', { exact: true }).first()).toBeVisible({ timeout: 20000 });
await expect(page.locator(`[data-testid="daemon-card-${serverId}"]:visible`).first()).toBeVisible({
timeout: 20000,
});
await page.goto(`/?serverId=${encodeURIComponent(serverId)}`);
await expect(page.getByText('New agent', { exact: true }).first()).toBeVisible();
@@ -87,20 +78,11 @@ test('new agent auto-selects first online host when no preference is stored', as
}
const nowIso = new Date().toISOString();
const testDaemon = {
const testDaemon = buildSeededHost({
serverId,
label: 'localhost',
connections: [
{
id: `direct:127.0.0.1:${daemonPort}`,
type: 'direct',
endpoint: `127.0.0.1:${daemonPort}`,
},
],
preferredConnectionId: `direct:127.0.0.1:${daemonPort}`,
createdAt: nowIso,
updatedAt: nowIso,
};
endpoint: `127.0.0.1:${daemonPort}`,
nowIso,
});
await gotoHome(page);
await page.evaluate(
+1 -1
View File
@@ -72,7 +72,7 @@ test('manual host add accepts host:port only and persists a direct connection',
entry?.serverId === serverId &&
Array.isArray(entry?.connections) &&
entry.connections.some(
(conn: any) => conn?.type === 'direct' && conn?.endpoint === `127.0.0.1:${port}`
(conn: any) => conn?.type === 'directTcp' && conn?.endpoint === `127.0.0.1:${port}`
)
);
} catch {
@@ -1,4 +1,5 @@
import { test, expect } from './fixtures';
import { buildDirectTcpConnection } from './helpers/daemon-registry';
import { gotoHome, openSettings } from './helpers/app';
test('connects via relay when direct endpoints fail', async ({ page }) => {
@@ -18,7 +19,7 @@ test('connects via relay when direct endpoints fail', async ({ page }) => {
serverId,
label: 'relay-daemon',
connections: [
{ id: 'direct:127.0.0.1:9', type: 'direct', endpoint: '127.0.0.1:9' },
buildDirectTcpConnection('127.0.0.1:9'),
{ id: `relay:${relayEndpoint}`, type: 'relay', relayEndpoint, daemonPublicKeyB64 },
],
preferredConnectionId: 'direct:127.0.0.1:9',
+2 -1
View File
@@ -1,4 +1,5 @@
import { test, expect } from './fixtures';
import { buildDirectTcpConnection } from './helpers/daemon-registry';
import { gotoHome, openSettings } from './helpers/app';
test('relay connection stays stable across multiple tabs', async ({ page }) => {
@@ -18,7 +19,7 @@ test('relay connection stays stable across multiple tabs', async ({ page }) => {
serverId,
label: 'relay-daemon',
connections: [
{ id: 'direct:127.0.0.1:9', type: 'direct', endpoint: '127.0.0.1:9' },
buildDirectTcpConnection('127.0.0.1:9'),
{ id: `relay:${relayEndpoint}`, type: 'relay', relayEndpoint, daemonPublicKeyB64 },
],
preferredConnectionId: 'direct:127.0.0.1:9',
@@ -0,0 +1,14 @@
import { afterEach, describe, expect, it } from "vitest";
import { __setAttachmentStoreForTests, getAttachmentStore } from "./store";
describe("attachment store", () => {
afterEach(() => {
__setAttachmentStoreForTests(null);
});
it("creates the default web attachment store without runtime module resolution errors", async () => {
const store = await getAttachmentStore();
expect(store.storageType).toBe("web-indexeddb");
});
});
+6 -6
View File
@@ -7,20 +7,20 @@ let attachmentStorePromise: Promise<AttachmentStore> | null = null;
async function createAttachmentStore(): Promise<AttachmentStore> {
if (Platform.OS === "web") {
if (isTauriEnvironment()) {
const { createDesktopAttachmentStore } = require(
"@/desktop/attachments/desktop-attachment-store"
const { createDesktopAttachmentStore } = await import(
"../desktop/attachments/desktop-attachment-store"
);
return createDesktopAttachmentStore();
}
const { createIndexedDbAttachmentStore } = require(
"@/attachments/web/indexeddb-attachment-store"
const { createIndexedDbAttachmentStore } = await import(
"./web/indexeddb-attachment-store"
);
return createIndexedDbAttachmentStore();
}
const { createNativeFileAttachmentStore } = require(
"@/attachments/native/native-file-attachment-store"
const { createNativeFileAttachmentStore } = await import(
"./native/native-file-attachment-store"
);
return createNativeFileAttachmentStore();
}
+23 -30
View File
@@ -15,6 +15,14 @@ export type SelectBestConnectionInput = {
probeByConnectionId: Map<string, ConnectionProbeState>;
};
function getAvailableLatency(input: {
connectionId: string;
probeByConnectionId: Map<string, ConnectionProbeState>;
}): number | null {
const probe = input.probeByConnectionId.get(input.connectionId);
return probe?.status === "available" ? probe.latencyMs : null;
}
export function selectBestConnection(
input: SelectBestConnectionInput
): string | null {
@@ -23,37 +31,22 @@ export function selectBestConnection(
return null;
}
const available = candidates
.map((candidate, index) => ({ candidate, index }))
.filter(({ candidate }) => {
const probe = probeByConnectionId.get(candidate.connectionId);
return probe?.status === "available";
let bestConnectionId: string | null = null;
let bestLatency: number | null = null;
for (const candidate of candidates) {
const latencyMs = getAvailableLatency({
connectionId: candidate.connectionId,
probeByConnectionId,
});
const byLatency = (left: { candidate: ConnectionCandidate; index: number }, right: { candidate: ConnectionCandidate; index: number }) => {
const leftProbe = probeByConnectionId.get(left.candidate.connectionId);
const rightProbe = probeByConnectionId.get(right.candidate.connectionId);
const leftLatency =
leftProbe && leftProbe.status === "available"
? leftProbe.latencyMs
: Number.POSITIVE_INFINITY;
const rightLatency =
rightProbe && rightProbe.status === "available"
? rightProbe.latencyMs
: Number.POSITIVE_INFINITY;
if (leftLatency === rightLatency) {
return left.index - right.index;
}
return leftLatency - rightLatency;
};
if (available.length === 0) {
return null;
if (latencyMs === null) {
continue;
}
if (bestLatency === null || latencyMs < bestLatency) {
bestConnectionId = candidate.connectionId;
bestLatency = latencyMs;
}
}
const sorted = available.sort(byLatency);
return sorted[0]!.candidate.connectionId;
return bestConnectionId;
}
+17 -12
View File
@@ -14,6 +14,15 @@ export interface SidebarShortcutModel {
shortcutIndexByWorkspaceKey: Map<string, number>
}
function createShortcutTarget(
workspace: SidebarWorkspaceEntry,
): SidebarShortcutWorkspaceTarget {
return {
serverId: workspace.serverId,
workspaceId: workspace.workspaceId,
}
}
export function buildSidebarShortcutModel(input: {
projects: SidebarProjectEntry[]
collapsedProjectKeys: ReadonlySet<string>
@@ -30,19 +39,15 @@ export function buildSidebarShortcutModel(input: {
}
for (const workspace of project.workspaces) {
visibleTargets.push({
serverId: workspace.serverId,
workspaceId: workspace.workspaceId,
})
const shortcutNumber =
shortcutTargets.length < maxShortcuts ? shortcutTargets.length + 1 : null
if (shortcutNumber !== null) {
shortcutTargets.push({
serverId: workspace.serverId,
workspaceId: workspace.workspaceId,
})
shortcutIndexByWorkspaceKey.set(workspace.workspaceKey, shortcutNumber)
visibleTargets.push(createShortcutTarget(workspace))
if (shortcutTargets.length >= maxShortcuts) {
continue
}
const shortcutNumber = shortcutTargets.length + 1
shortcutTargets.push(createShortcutTarget(workspace))
shortcutIndexByWorkspaceKey.set(workspace.workspaceKey, shortcutNumber)
}
}
+21 -7
View File
@@ -27,15 +27,29 @@ export default defineConfig({
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"@server": path.resolve(__dirname, "../server/src"),
alias: [
{
find: /^@getpaseo\/relay\/e2ee$/,
replacement: path.resolve(__dirname, "../relay/src/e2ee.ts"),
},
{
find: /^@getpaseo\/relay$/,
replacement: path.resolve(__dirname, "../relay/src/index.ts"),
},
{ find: "@", replacement: path.resolve(__dirname, "src") },
{ find: "@server", replacement: path.resolve(__dirname, "../server/src") },
// Point to the ESM build so Vite can transform its imports and apply the
// react alias below (the CJS build uses require('react') which bypasses
// Vite alias resolution).
"react-native": path.resolve(rootNodeModules, "react-native-web/dist/index.js"),
react: path.resolve(appNodeModules, "react"),
"react-dom": path.resolve(appNodeModules, "react-dom"),
},
{
find: "react-native",
replacement: path.resolve(rootNodeModules, "react-native-web/dist/index.js"),
},
{ find: "react", replacement: path.resolve(appNodeModules, "react") },
{
find: "react-dom",
replacement: path.resolve(appNodeModules, "react-dom"),
},
],
},
});