fix: suppress telemetry consent prompt in Cloud Canvas (#1848)

* Suppress telemetry consent modal in Cloud Canvas

Treat same-origin locked Cloud cookie deployments as already consented for Canvas library telemetry so the modal does not flicker before the main app login flow.

Co-authored-by: openhands <openhands@all-hands.dev>

* Stabilize mock LLM settings tests after ACP

Reset the default agent profile back to OpenHands through the agent-profile API before LLM-profile setup paths that need /settings/llm. This prevents an ACP profile left by the previous serial spec from redirecting later settings tests to /settings/agents.

Co-authored-by: openhands <openhands@all-hands.dev>

* Stabilize files tab mock E2E git setup

Ensure the attached-workspace conversation has both an origin remote and a real HEAD commit before asserting that the Files tab defaults to diff view. The diff default now intentionally depends on both attached source metadata and an available commit base.

Co-authored-by: openhands <openhands@all-hands.dev>

* Fix desktop right panel toggle visibility

Update the desktop right-panel toggle to set both the user-toggled flag and the visible state. The missing visibility update left the panel visually closed in mock E2E while off-screen tab controls remained mounted.

Co-authored-by: openhands <openhands@all-hands.dev>

* Make files tab mock E2E open the panel explicitly

Wait for the desktop panel toggle to report an open state before interacting with Files tab controls, then verify the Diff segment can be selected for the attached-workspace conversation.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Rohit Malhotra
2026-07-20 15:33:24 +00:00
committed by GitHub
co-authored by openhands
parent 4e9ea1334a
commit efd20f7d56
8 changed files with 122 additions and 62 deletions
@@ -5,7 +5,7 @@
* agent-server with a scripted mock LLM backend.
*
* Coverage (issue #511):
* - Files tab defaults to diff view when a workspace is attached
* - Files tab diff view can be enabled when a workspace is attached
* - Git control bar shows workspace-name pill for folder-attached conversations
* - Browser tab renders empty state when no page has been browsed
* - Files tab defaults to file-tree view when NO workspace is attached
@@ -114,14 +114,13 @@ test.describe("files tab, git control bar, and browser tab", () => {
// Register a trajectory that ensures the workspace has a git remote.
// The npm path inherits the host repo; the Docker path bootstraps one.
const gitBootstrap = [
// Skip if already in a repo with an origin remote (npm worktree path)
"git remote get-url origin >/dev/null 2>&1",
// Otherwise bootstrap a fresh repo with a GitHub remote (Docker path).
"git rev-parse --is-inside-work-tree >/dev/null 2>&1 || git init",
// Must configure user.name/email — Docker containers may not have them.
"|| (git init && git config user.email test@test.com && git config user.name test",
`&& git remote add origin https://github.com/${EXPECTED_REPO_SLUG}.git`,
"&& git commit --allow-empty -m init)",
].join(" ");
"git config user.email test@test.com",
"git config user.name test",
`git remote get-url origin >/dev/null 2>&1 || git remote add origin https://github.com/${EXPECTED_REPO_SLUG}.git`,
"git rev-parse --verify HEAD >/dev/null 2>&1 || git commit --allow-empty -m init",
].join(" && ");
await registerTrajectory(request, "files-and-git", [
{
tool_call: {
@@ -153,7 +152,8 @@ test.describe("files tab, git control bar, and browser tab", () => {
await page.evaluate(
({ testId, text }) => {
const el = document.querySelector(`[data-testid="${testId}"]`);
if (!(el instanceof HTMLElement)) throw new Error("Chat input not found");
if (!(el instanceof HTMLElement))
throw new Error("Chat input not found");
el.focus();
el.textContent = text;
el.dispatchEvent(
@@ -214,7 +214,8 @@ test.describe("files tab, git control bar, and browser tab", () => {
// Accept either so the assertion doesn't race that probe (the source of a
// pre-existing flake — see GitControlBarRepoButton: selectedRepository ||
// workspaceName).
const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const escapeRegExp = (s: string) =>
s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pillText = new RegExp(
`${escapeRegExp(workspaceName)}|${escapeRegExp(EXPECTED_REPO_SLUG)}`,
);
@@ -239,14 +240,16 @@ test.describe("files tab, git control bar, and browser tab", () => {
} catch {
// Soft-fail: git probe may not have completed in time (Docker).
// The workspace pill assertion above is the primary gate.
console.log("Pull/Push buttons not visible — git probe likely still pending");
console.log(
"Pull/Push buttons not visible — git probe likely still pending",
);
}
});
});
// ── Step 4: Verify Files tab diff toggle defaults to "on" ──────────
// ── Step 4: Verify Files tab diff toggle can be enabled ─────────────
test("step 4: files tab defaults to diff view for attached workspace", async ({
test("step 4: files tab can enable diff view for attached workspace", async ({
page,
}) => {
test.skip(!attachedConversationId, "step 2 must complete first");
@@ -265,12 +268,13 @@ test.describe("files tab, git control bar, and browser tab", () => {
// Open the right panel
const toggle = page.getByTestId("right-panel-toggle");
await expect(toggle).toBeVisible({ timeout: 10_000 });
await toggle.click();
await toggle.click({ force: true });
await expect(toggle).toHaveAttribute("aria-pressed", "true", {
timeout: 10_000,
});
// Wait for at least one tab to be visible (panel animation done)
const anyTab = page.locator(
'[data-testid^="conversation-tab-"]',
).first();
const anyTab = page.locator('[data-testid^="conversation-tab-"]').first();
await expect(anyTab).toBeVisible({ timeout: 10_000 });
// Click the Files tab
@@ -284,12 +288,9 @@ test.describe("files tab, git control bar, and browser tab", () => {
await expect(diffToggle).toBeVisible({ timeout: 15_000 });
const diffOnOption = page.getByTestId("files-tab-diff-toggle-option-on");
// Verify the toggle is interactive: click "on" with force to bypass
// any residual animation overlay, and confirm it becomes checked.
await diffOnOption.click({ force: true });
await expect(diffOnOption).toHaveAttribute("aria-checked", "true", {
timeout: 5_000,
timeout: 15_000,
});
});
});
@@ -352,7 +353,8 @@ test.describe("files tab, git control bar, and browser tab", () => {
await page.evaluate(
({ testId, text }) => {
const el = document.querySelector(`[data-testid="${testId}"]`);
if (!(el instanceof HTMLElement)) throw new Error("Chat input not found");
if (!(el instanceof HTMLElement))
throw new Error("Chat input not found");
el.focus();
el.textContent = text;
el.dispatchEvent(
@@ -398,7 +400,9 @@ test.describe("files tab, git control bar, and browser tab", () => {
await expect(diffToggle).toBeVisible({ timeout: 15_000 });
// Without an attached workspace, the "off" (Files) option should be active
const diffOffOption = page.getByTestId("files-tab-diff-toggle-option-off");
const diffOffOption = page.getByTestId(
"files-tab-diff-toggle-option-off",
);
await expect(diffOffOption).toBeVisible({ timeout: 10_000 });
await expect(diffOffOption).toHaveAttribute("aria-checked", "true");
});
@@ -32,6 +32,7 @@ import {
resetToOpenHandsAgentViaUI,
resetMockLLM,
ensureMockLLMProfile,
ensureMockLLMAgentProfile,
openAgentProfileEditor,
selectDropdownOption,
setChatInput,
@@ -75,6 +76,7 @@ test.describe("mock-LLM ACP agent conversation", () => {
const page = await browser.newPage();
try {
await seedLocalStorage(page);
await ensureMockLLMAgentProfile(page.request);
await resetToOpenHandsAgentViaUI(page);
await ensureMockLLMProfile(page);
} catch {
@@ -107,11 +109,7 @@ test.describe("mock-LLM ACP agent conversation", () => {
// ── Switch agent type from OpenHands → ACP ──
await test.step("select ACP agent type", async () => {
await selectDropdownOption(
page,
/Agent/,
/ACP/,
);
await selectDropdownOption(page, /Agent/, /ACP/);
});
// ── After selecting ACP, the preset dropdown + command fields appear ──
@@ -121,11 +119,7 @@ test.describe("mock-LLM ACP agent conversation", () => {
await waitForTestId(page, "agent-preset-selector");
// Select "Custom" preset so we can enter our own command
await selectDropdownOption(
page,
/Preset/,
/Custom/,
);
await selectDropdownOption(page, /Preset/, /Custom/);
// Fill in the ACP command pointing to our mock server
const commandInput = page.getByTestId("agent-command-input");
@@ -42,6 +42,7 @@ import {
createProfileViaUI,
deleteProfileIfExists,
activateProfileViaUI,
ensureMockLLMAgentProfile,
} from "../utils/mock-llm-helpers";
const MOCK_MODEL = "openai/mock-test-model";
@@ -88,6 +89,7 @@ test.describe("active profile deletion + reconciliation", () => {
try {
await seedLocalStorage(page);
await routeSessionApiKey(page);
await ensureMockLLMAgentProfile(page.request);
await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
await dismissAnalyticsModal(page);
await waitForTestId(page, "add-llm-profile");
@@ -105,6 +107,7 @@ test.describe("active profile deletion + reconciliation", () => {
}) => {
// ── Setup: create two profiles via the UI, activate one ──
await routeSessionApiKey(page);
await ensureMockLLMAgentProfile(page.request);
await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
await dismissAnalyticsModal(page);
await waitForTestId(page, "add-llm-profile");
@@ -260,6 +263,7 @@ test.describe("same-model profile identity", () => {
try {
await seedLocalStorage(page);
await routeSessionApiKey(page);
await ensureMockLLMAgentProfile(page.request);
await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
await dismissAnalyticsModal(page);
await waitForTestId(page, "add-llm-profile");
@@ -286,6 +290,7 @@ test.describe("same-model profile identity", () => {
// ── Setup: create both profiles with the same model via the UI,
// then activate BETA through the profile menu ──
await routeSessionApiKey(page);
await ensureMockLLMAgentProfile(page.request);
await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
await dismissAnalyticsModal(page);
await waitForTestId(page, "add-llm-profile");
@@ -384,6 +389,7 @@ test.describe("OpenHands provider hidden base_url preservation", () => {
try {
await seedLocalStorage(page);
await routeSessionApiKey(page);
await ensureMockLLMAgentProfile(page.request);
await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
await dismissAnalyticsModal(page);
await waitForTestId(page, "add-llm-profile");
@@ -403,6 +409,7 @@ test.describe("OpenHands provider hidden base_url preservation", () => {
// Advanced view. The value becomes hidden after switching to Basic, but it
// is still part of the profile unless the model changes. ──
await routeSessionApiKey(page);
await ensureMockLLMAgentProfile(page.request);
await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
await dismissAnalyticsModal(page);
await waitForTestId(page, "add-llm-profile");
+63 -29
View File
@@ -337,11 +337,17 @@ async function retryOnTransient(
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const resp =
method === "GET" ? await request.get(url, options) :
method === "PATCH" ? await request.patch(url, options) :
method === "POST" ? await request.post(url, options) :
await request.delete(url, options);
if ((resp.status() === 502 || resp.status() === 503) && attempt < retries) {
method === "GET"
? await request.get(url, options)
: method === "PATCH"
? await request.patch(url, options)
: method === "POST"
? await request.post(url, options)
: await request.delete(url, options);
if (
(resp.status() === 502 || resp.status() === 503) &&
attempt < retries
) {
await new Promise((r) => setTimeout(r, delayMs));
continue;
}
@@ -357,7 +363,12 @@ async function retryOnTransient(
throw err;
}
}
throw (lastError ?? new Error(`retryOnTransient: exhausted ${retries} attempts for ${method} ${url}`));
throw (
lastError ??
new Error(
`retryOnTransient: exhausted ${retries} attempts for ${method} ${url}`,
)
);
}
/**
@@ -371,12 +382,17 @@ export async function ensureMockLLMProfileViaAPI(
request: APIRequestContext,
model = "openai/mock-test-model",
) {
const settingsResp = await retryOnTransient(request, "GET", `${BACKEND_URL}/api/settings`, {
headers: {
"X-Session-API-Key": SESSION_API_KEY,
"X-Expose-Secrets": "encrypted",
const settingsResp = await retryOnTransient(
request,
"GET",
`${BACKEND_URL}/api/settings`,
{
headers: {
"X-Session-API-Key": SESSION_API_KEY,
"X-Expose-Secrets": "encrypted",
},
},
});
);
if (settingsResp.ok()) {
const settings = await settingsResp.json();
@@ -386,21 +402,26 @@ export async function ensureMockLLMProfileViaAPI(
}
}
const patchResp = await retryOnTransient(request, "PATCH", `${BACKEND_URL}/api/settings`, {
headers: {
"X-Session-API-Key": SESSION_API_KEY,
"Content-Type": "application/json",
},
data: {
agent_settings_diff: {
llm: {
model,
api_key: "mock-api-key-for-testing",
base_url: MOCK_LLM_AGENT_URL,
const patchResp = await retryOnTransient(
request,
"PATCH",
`${BACKEND_URL}/api/settings`,
{
headers: {
"X-Session-API-Key": SESSION_API_KEY,
"Content-Type": "application/json",
},
data: {
agent_settings_diff: {
llm: {
model,
api_key: "mock-api-key-for-testing",
base_url: MOCK_LLM_AGENT_URL,
},
},
},
},
});
);
expect(
patchResp.ok(),
`PATCH /api/settings failed: ${patchResp.status()}`,
@@ -433,6 +454,12 @@ export async function ensureMockLLMProfile(
} = {},
) {
await routeSessionApiKey(page);
try {
await ensureMockLLMAgentProfile(page.request, profileName);
} catch {
// The target LLM profile may not exist yet on clean state. In that case the
// default agent is already OpenHands, so the LLM settings route stays usable.
}
await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
await dismissAnalyticsModal(page);
await waitForTestId(page, "add-llm-profile");
@@ -784,7 +811,12 @@ export async function registerTrajectory(
request: APIRequestContext,
name: string,
turns: Array<
| { tool_call: { name: string; arguments: Record<string, unknown> | string } }
| {
tool_call: {
name: string;
arguments: Record<string, unknown> | string;
};
}
| { text: string }
>,
) {
@@ -795,7 +827,9 @@ export async function registerTrajectory(
headers: { "Content-Type": "application/json" },
},
);
expect(resp.ok(), `Register trajectory "${name}": ${resp.status()}`).toBe(true);
expect(resp.ok(), `Register trajectory "${name}": ${resp.status()}`).toBe(
true,
);
}
/**
@@ -812,7 +846,9 @@ export async function activateTrajectory(
headers: { "Content-Type": "application/json" },
},
);
expect(resp.ok(), `Activate trajectory "${name}": ${resp.status()}`).toBe(true);
expect(resp.ok(), `Activate trajectory "${name}": ${resp.status()}`).toBe(
true,
);
}
/**
@@ -935,9 +971,7 @@ export const MOCK_ACP_COMMAND_SCRIPT =
* @deprecated Use `resetToOpenHandsAgentViaUI(page)` to exercise the UI path.
* Kept only for callers that cannot open a page (should not exist in new tests).
*/
export async function resetToOpenHandsAgent(
request: APIRequestContext,
) {
export async function resetToOpenHandsAgent(request: APIRequestContext) {
const resp = await request.patch(`${BACKEND_URL}/api/settings`, {
headers: {
"X-Session-API-Key": SESSION_API_KEY,