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', () => { assert.equal(typeof prepareAction, 'function'); assert.equal(prepareAction(req()).url, 'https://chatgpt.com/?q=Hello'); assert.equal(prepareAction(req({actionId:'translate'})).kind,'language'); assert.match(new URL(prepareAction(req({actionId:'translate',language:'Spanish'})).url).searchParams.get('q'),/Spanish/); assert.equal(prepareAction(req({actionId:'translate',settings:{...DEFAULT_SETTINGS,translationMode:'preferred',translationLanguage:'Spanish'}})).kind,'ready'); assert.equal(prepareAction(req({source:''})).url,'https://chatgpt.com/'); assert.equal(prepareAction(req({actionId:'debug',source:''})).kind,'error'); assert.equal(prepareAction(req({actionId:'bogus'})).kind,'error'); }); test('URLs are validated and preserved whole, never shortened', () => { assert.equal(typeof prepareAction, 'function'); for (const source of ['javascript:alert(1)','about:config','ftp://example.com','https://user:pass@example.com','not a url','https://example.com/?q='+'x'.repeat(9000)]) { assert.equal(prepareAction(req({actionId:'ask-link',source})).kind,'error',source.slice(0,40)); } const source='https://example.com/?q=a%20b&x=1#part'; const out=prepareAction(req({actionId:'review-link',source})); assert.equal(out.kind,'ready'); assert.ok(new URL(out.url).searchParams.get('q').endsWith(source)); }); test('shortened selections end on a whole word and decode cleanly', () => { assert.equal(typeof prepareAction, 'function'); const out=prepareAction(req({source:'lorem ipsum '.repeat(2000)})); assert.equal(out.kind,'shortened'); const words=new URL(out.url).searchParams.get('q').replace('[Selected text shortened]','').trim().split(/\s+/); assert.ok(words.length>1); for (const word of words) assert.ok(word==='lorem'||word==='ipsum',`cut mid-word: ${word}`); const emoji=prepareAction(req({source:'😀 '.repeat(9000)})); assert.equal(emoji.kind,'shortened'); assert.doesNotThrow(()=>decodeURIComponent(new URL(emoji.url).search.slice(1))); }); test('long selections preserve instructions, repetition and Unicode with explicit consent', () => { assert.equal(typeof prepareAction, 'function'); const settings={...DEFAULT_SETTINGS,promptTemplate:'START {query} AGAIN {query} END',temporaryChat:true,webSearch:true}; const out=prepareAction(req({source:'😀 word '.repeat(4000),settings})); assert.equal(out.kind,'shortened'); assert.ok(out.url.length<=8000); const q=new URL(out.url).searchParams.get('q'); assert.ok(q.startsWith('START '));assert.ok(q.includes(' END'));assert.ok(q.includes('[Selected text shortened]')); assert.equal(q.match(/START (.*?) AGAIN/s)[1],q.match(/AGAIN (.*?) END/s)[1]); assert.equal(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? { 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]')); });