From 94e3bfd5d101634978d299a12b7ddde725f6291f Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 26 Aug 2026 13:44:32 +0800 Subject: [PATCH 1/2] test(web): gate composer gestures on the editable attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A running turn disables the composer by flipping contenteditable to false on the same element. fill() throws there immediately — a disabled textarea used to hold it back through actionability — and isEnabled() reports true for a div regardless, so the permission-policy scenario's post-settle wait never waited and its next gesture raced the re-enable render. The window is a few frames wide; #3083's Remote-routed subagent control stretches settle enough to hit it on CI. writeComposerDraft now waits for contenteditable="true" before acting, and the permission-policy scenario drives all four sends through it with the settle wait pinned to the attribute. --- apps/web/tests/permission-policy-context.e2e.ts | 12 ++++++------ apps/web/tests/support.ts | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/web/tests/permission-policy-context.e2e.ts b/apps/web/tests/permission-policy-context.e2e.ts index 3ea7507550..f900990fab 100644 --- a/apps/web/tests/permission-policy-context.e2e.ts +++ b/apps/web/tests/permission-policy-context.e2e.ts @@ -15,7 +15,7 @@ import { assertFinalWorkspaceSnapshot, assertFixtureInventory, fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' +import { connectFreshWorkspace, newEnglishPage, saveFailureShot, writeComposerDraft } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/permission-policy-context', import.meta.url)) const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/permission-policy-context/session.jsonl', import.meta.url)) @@ -97,23 +97,23 @@ describe('web e2e: current sandbox policy reaches the model before tools', () => const input = page.locator('[data-composer-input]').first() let sessionId: Awaited> | undefined for (const [index, preset] of ['read-only', 'danger-full-access', 'workspace-write'].entries()) { - await input.fill(`/permission ${preset}`) + await writeComposerDraft(page, input, `/permission ${preset}`) await input.press('Enter') await page.getByRole('button', { name: `Access mode, current: ${PRESET_LABELS[index]}` }) .waitFor({ timeout: 10_000 }) const settled = scaffold.whenTurnSettled() - await input.fill(PROMPTS[index] as string) + await writeComposerDraft(page, input, PROMPTS[index] as string) await input.press('Enter') sessionId = await settled - await expect.poll(() => input.isEnabled(), { timeout: 10_000 }).toBe(true) + await expect.poll(() => input.getAttribute('contenteditable'), { timeout: 10_000 }).toBe('true') } - await input.fill('/permission read-only') + await writeComposerDraft(page, input, '/permission read-only') await input.press('Enter') await page.getByRole('button', { name: 'Access mode, current: Read Only' }).waitFor({ timeout: 10_000 }) const settled = scaffold.whenTurnSettled() - await input.fill(PROMPTS[3]) + await writeComposerDraft(page, input, PROMPTS[3]) await input.press('Enter') sessionId = await settled diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts index b43fcbaa76..5ff972ff8a 100644 --- a/apps/web/tests/support.ts +++ b/apps/web/tests/support.ts @@ -117,6 +117,13 @@ export async function connectFreshWorkspaceZh(page: Page, root: string, name = ' * selection, and the batched edit lands on a null selection and is silently * dropped, leaving the previous draft in place. Real keystrokes leave room for * `selectionchange` between keys, which is also what a user's typing does. + * + * Waits for the surface to be editable first. A running turn disables the + * composer by setting `contenteditable="false"` on the same element, where + * `fill()` throws immediately instead of waiting (a disabled `