fix: cap huge selections before shortening

The shortcut and toolbar read window.getSelection() without a limit, so a
select-all on a very large page handed prepareAction millions of
characters. Every binary-search probe re-encoded the whole candidate
prefix: about 1.1 s for 5M characters, paid again by the dialog.

Each code point adds at least one character to the URL, so nothing past
the first MAX_URL_CHARS can fit. Cap there before any encoding. A capped
source never takes the ready path: renderPrompt trims, so a prefix of
mostly whitespace could fit and silently drop the rest without consent.

Output is unchanged: 2,000 random cases matched develop exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
golem
2026-10-18 01:32:22 -06:00
co-authored by Claude Opus 5.5
parent 99bb58f7b7
commit 519131c21f
3 changed files with 36 additions and 3 deletions
+20
View File
@@ -1,6 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { DEFAULT_SETTINGS } from '../lib/defaults.js';
import { MAX_URL_CHARS } from '../lib/build-url.js';
const { prepareAction } = await import('../lib/prepare-action.js').catch(()=>({}));
const req = (overrides={}) => ({actionId:'ask',source:'Hello',settings:{...DEFAULT_SETTINGS},...overrides});
test('ready actions preserve source and hints, translation asks only when needed', () => {
@@ -38,3 +39,22 @@ test('long selections preserve instructions, repetition and Unicode with explici
const translated=prepareAction(req({actionId:'translate',language:'Spanish',source:'x'.repeat(20000)}));
assert.equal(translated.kind,'shortened');assert.match(new URL(translated.url).searchParams.get('q'), /Spanish/);
});
test('huge selections are capped before shortening, with identical output', () => {
assert.equal(typeof prepareAction, 'function');
// Select-all on a very large page; shortening this took over a second uncapped.
const source='😀 word '.repeat(625000);
const started=performance.now();
const out=prepareAction(req({source}));
const elapsed=performance.now()-started;
assert.equal(out.kind,'shortened');
assert.ok(elapsed<250,`took ${elapsed.toFixed(0)} ms`);
const capped=Array.from(source).slice(0,MAX_URL_CHARS).join('');
assert.deepEqual(prepareAction(req({source:capped})),out);
});
test('a selection too long to send whole always needs consent, even if its start would fit', () => {
assert.equal(typeof prepareAction, 'function');
// The first MAX_URL_CHARS code points trim down to "a"; sending just that would drop the rest unasked.
const out=prepareAction(req({source:'a'+' '.repeat(MAX_URL_CHARS)+'b'}));
assert.equal(out.kind,'shortened');
assert.ok(new URL(out.url).searchParams.get('q').includes('[Selected text shortened]'));
});