Files
hapi/e2e/composer-copy.spec.ts
T
ca1fd80afd fix(cursor): pin Auto to CLI --model auto instead of account default (#1819)
* fix(cursor): pin Auto to CLI --model auto instead of account default

Selecting Auto was treated as unset/ACP default[] after the Default→Auto rename. Persist and spawn `auto` so the picker matches `agent --list-models`.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): keep live ACP model until Auto actually applies

Pin session.model to auto for the next `--model auto` spawn, but do not claim the live backend switched when ACP only advertises default[].

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): reject live Auto unless ACP auto or spawn confirmed it

Do not persist Auto when the running catalog has no auto option. Clear the spawn-auto flag after a concrete model apply so Auto → concrete → Auto cannot fake a live switch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): keep ACP wire ids spawn-safe and apply effort

Cursor's parameterized picker (`clientCapabilities._meta.parameterizedModelPicker`)
advertises bare model bases plus separate `fast` / `thought_level` config options.
`buildCursorModelsSnapshotFromAcp` synthesized every base as
`${base}[fast=${fast}]`, and Cursor rejects any bracket id whose parameter set is
not that model's complete set, so 74 of the 76 published picker ids failed at ACP
`session/new` with `Cannot use this model` (#1818).

- Stop synthesizing wire ids: keep the advertised values verbatim (bare bases are
  accepted by `--model`, defaults applied). Parameter sets differ per model, so a
  synthesized id cannot be validated from the options alone.
- Flag such catalogs as `parameterized` so variant CLI skus still attach to their
  base rows — the ACP apply path expresses them through config options.
- Apply effort in `applyParameterizedCursorModel` (ids `effort`, `reasoning`,
  `reasoning_effort`), applied against the config options Cursor re-advertises for
  the newly selected model, and skipped on models without that axis.
- Match CLI sku bases across Cursor's legacy `cursor-` family prefix
  (`cursor-grok-4.6-high` → ACP base `grok-4.6`) so those variant rows are not
  dropped.
- Version the shared on-disk catalog cache (v2) so runners that already cached the
  rejected synthesized ids re-probe instead of serving them.

Verified with live `cursor-agent 2026.09.10-fd3934a` ACP runs: the catalog no
longer contains bracket ids, every advertised base and sampled variant sku spawns,
and in-session `claude-opus-4-8-low` / `cursor-grok-4.6-low` set the matching
`effort`/`reasoning` option.

* test(cursor): cover aliased effort ids and thinking coexistence

Covers the branches the HAPI bot review called out for #1818: the
`xhigh` → `extra-high` alias on `gpt-5.5` (`reasoning`), the
`reasoning_effort` id used by `gemini-3.8-flash`, and switching effort on a
model that also advertises Claude's separate `thought_level` `thinking`
toggle.

* test(cursor): cover legacy sku base and post-switch effort option

Adds the two apply-path cases the HAPI bot review called out for #1818:
resolving `cursor-grok-4.6-high` onto an advertised `grok-4.6` base, and
reading the effort option Cursor advertises for the newly selected model
(`reasoning` for gpt-5.5 → `effort` for claude-opus-4-8) instead of the
previous model's options.

* test(cursor): create the cache dir in the migration test

The version-envelope test wrote $HAPI_HOME/cache/cursor-models.json without
creating the directory, so running it alone (or filtering with -t) failed with
ENOENT before the assertion; it only passed because sibling tests in the same
file had already created the path. Reported in the HAPI bot review of #1822.

Verified: `vitest run ... -t 'ignores pre-versioned'` → 1 passed, whole file → 4 passed,
and every other changed test file passes when run standalone.

* Clarify Cursor Auto live-switch limitation in session pickers

* Reconcile partial Cursor model changes before acknowledging Auto

* Serialize Cursor model changes across clients with the existing async lock

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 08:12:16 +08:00

65 lines
3.0 KiB
TypeScript

import { expect, test } from '@playwright/test'
/**
* Regression spec for the Chromium select-all quirk: a page containing a
* contenteditable (the rich composer) makes Ctrl+A collapse to an empty
* caret when focus is outside the editable, so Ctrl+C copies nothing.
* SessionChat's applyGlobalSelectAll takeover must restore the expected
* "select the conversation" behavior while leaving composer/input
* select-all to the browser.
*/
test.describe('composer Ctrl+A + Ctrl+C copy', () => {
test.beforeEach(async ({ context, page }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
await page.goto('/e2e-fixtures/composer-copy-fixture.html')
await page.locator('[data-testid="rich-composer-input"]').waitFor()
})
test('Ctrl+A outside the composer selects the message thread and Ctrl+C copies it', async ({ page }) => {
await page.keyboard.press('ControlOrMeta+a')
const selected = await page.evaluate(() => window.getSelection()?.toString() ?? '')
expect(selected).toContain('The quick brown fox jumps over the lazy dog')
expect(selected).toContain('my earlier user message')
await page.keyboard.press('ControlOrMeta+c')
const copied = await page.evaluate(() => navigator.clipboard.readText())
expect(copied).toBe(selected)
})
test('Ctrl+A with composer focused still selects only the composer draft', async ({ page }) => {
const editor = page.locator('[data-testid="rich-composer-input"]')
await editor.click()
await page.keyboard.type('my draft message')
await page.keyboard.press('ControlOrMeta+a')
await page.keyboard.press('ControlOrMeta+c')
const copied = await page.evaluate(() => navigator.clipboard.readText())
expect(copied).toBe('my draft message')
})
test('Ctrl+A in a textarea keeps native behavior', async ({ page }) => {
await page.evaluate(() => {
const textarea = document.createElement('textarea')
textarea.id = 'plain-textarea'
textarea.value = 'textarea draft'
document.body.appendChild(textarea)
})
const textarea = page.locator('#plain-textarea')
await textarea.click()
await page.keyboard.press('ControlOrMeta+a')
await page.keyboard.press('ControlOrMeta+c')
const copied = await page.evaluate(() => navigator.clipboard.readText())
expect(copied).toBe('textarea draft')
})
test('mouse-drag selection still copies via plain Ctrl+C', async ({ page }) => {
const msg = page.locator('[data-testid="assistant-message-2"]')
await msg.dragTo(page.locator('[data-testid="user-message-1"]'), {
sourcePosition: { x: 0, y: 2 },
targetPosition: { x: 300, y: 2 },
})
await page.keyboard.press('ControlOrMeta+c')
const copied = await page.evaluate(() => navigator.clipboard.readText())
expect(copied).toContain('Another assistant message')
expect(copied).toContain('my earlier user message')
})
})