Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b987685d89 | ||
|
|
3f4b64374b | ||
|
|
cf5e397d53 | ||
|
|
8052c87c23 | ||
|
|
9f8a295a4f | ||
|
|
5a12663c92 | ||
|
|
384eca7589 | ||
|
|
d96277ebd9 | ||
|
|
269e721fe8 | ||
|
|
39a3ac74f4 | ||
|
|
cb7da3a538 | ||
|
|
519131c21f |
@@ -2,13 +2,31 @@ name: Firefox Extension CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
# edited: removing the WIP: prefix starts the real run.
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
- main
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# The gate and concurrency use the github.* context, Gitea's alias for gitea.*:
|
||||||
|
# it is the form the pattern was canaried with and the one pr-flow detects.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
# Cancel superseded PR runs, never a push run: main's push uploads the package.
|
||||||
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
|
# WIP gate: skip PRs titled WIP: so early PRs cost no run per push. Gate on
|
||||||
|
# the title and nothing else, since a skipped job reports success on the
|
||||||
|
# required check (see ~/.claude/docs/gitea-ci-pattern.md).
|
||||||
|
if: >-
|
||||||
|
github.event_name != 'pull_request' || (
|
||||||
|
!startsWith(github.event.pull_request.title, 'WIP:') &&
|
||||||
|
!startsWith(github.event.pull_request.title, '[WIP]')
|
||||||
|
)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
|
|||||||
+16
-10
@@ -2,28 +2,34 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version).
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version).
|
||||||
|
|
||||||
## Unreleased
|
## [1.2.0] (2026-10-18)
|
||||||
|
|
||||||
|
The first update on addons.mozilla.org since 1.0. It also delivers everything
|
||||||
|
in 1.1.0, which was never published there.
|
||||||
|
|
||||||
- Add an Ask ChatGPT toolbar button: use selected text or open a blank chat,
|
- Add an Ask ChatGPT toolbar button: use selected text or open a blank chat,
|
||||||
honoring saved prompt and tab/window preferences. No new permissions.
|
honoring saved prompt and tab/window preferences. No new permissions.
|
||||||
|
Requires Firefox 112 or newer, like the other new entry points.
|
||||||
- Add Off / Simple / Advanced context-menu modes with migration from the old
|
- Add Off / Simple / Advanced context-menu modes with migration from the old
|
||||||
visibility preference. Advanced provides selection presets and link/page URL
|
visibility preference. Advanced provides selection presets and link/page URL
|
||||||
actions, with optional translation and a preferred language or language dialog.
|
actions, with optional translation and a preferred language or language dialog.
|
||||||
- Preserve dedicated action instructions when shortening selections; ask before
|
- Preserve dedicated action instructions when shortening selections; ask before
|
||||||
sending shortened text and reject URLs that cannot be sent intact.
|
sending shortened text and reject URLs that cannot be sent intact.
|
||||||
|
- Stay responsive with very large selections from the shortcut or toolbar: only
|
||||||
|
the part that can fit in the ChatGPT link is processed.
|
||||||
- Add accessible action dialogs with cancellation and retry, remove the temporary
|
- Add accessible action dialogs with cancellation and retry, remove the temporary
|
||||||
model/effort URL probe, and document URL sharing. No new permissions.
|
model/effort URL probe, and document URL sharing. No new permissions.
|
||||||
- Show up front, in the options panel and README, that ChatGPT may require
|
- Show up front, in the options panel and README, that ChatGPT may require
|
||||||
signing in and that an unsent prompt needs Enter or Send.
|
signing in and that an unsent prompt needs Enter or Send.
|
||||||
|
- Limit the prompt template to 1,000 characters, so an oversized template can
|
||||||
|
no longer stop every other setting from saving.
|
||||||
* Options instructions now show the configured keyboard shortcut, update when
|
- Show the configured keyboard shortcut in the options instructions, updating
|
||||||
it changes, and indicate when no shortcut is assigned. Added a button to
|
when it changes and indicating when none is assigned, with a button to open
|
||||||
open Firefox’s shortcut editor where supported.
|
Firefox’s shortcut editor where supported.
|
||||||
* Shortened the context menu label to “Ask ChatGPT”.
|
- Shorten the context menu label to “Ask ChatGPT”.
|
||||||
* Added quick usage instructions and guidance for changing the search keyword
|
- Add quick usage instructions and guidance for changing the search keyword
|
||||||
and default engine in Firefox to the options panel.
|
and default engine in Firefox to the options panel.
|
||||||
* Added a “Feedback or report a bug” link to the options panel and README.
|
- Add a “Feedback or report a bug” link to the options panel and README.
|
||||||
|
|
||||||
## [1.1.0] (2026-07-30)
|
## [1.1.0] (2026-07-30)
|
||||||
|
|
||||||
@@ -44,7 +50,7 @@ Adds an options panel and two new ways to reach ChatGPT.
|
|||||||
* Permissions added: storage, menus, activeTab. None of them adds a line to
|
* Permissions added: storage, menus, activeTab. None of them adds a line to
|
||||||
Firefox's install prompt.
|
Firefox's install prompt.
|
||||||
|
|
||||||
## [1.0.0]) (2024-06-09)
|
## [1.0.0] (2024-06-09)
|
||||||
|
|
||||||
First release of software.
|
First release of software.
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ Open `about:addons`, select this extension, and choose the **Options** tab
|
|||||||
guarantee.
|
guarantee.
|
||||||
- **Open in** — a new tab, a new background tab, or a new window.
|
- **Open in** — a new tab, a new background tab, or a new window.
|
||||||
- **Prompt template** — wraps your text, for example `Explain simply: {query}`.
|
- **Prompt template** — wraps your text, for example `Explain simply: {query}`.
|
||||||
|
Up to 1,000 characters.
|
||||||
|
|
||||||
Restore defaults resets extension preferences to Simple mode with translation
|
Restore defaults resets extension preferences to Simple mode with translation
|
||||||
hidden; it does not change Firefox's search engine or shortcut settings.
|
hidden; it does not change Firefox's search engine or shortcut settings.
|
||||||
|
|||||||
@@ -96,6 +96,21 @@ send. The hypothesis that address-bar navigation might still be favoured
|
|||||||
(`Sec-Fetch-Site: none` being the most-trusted value) **does not hold in
|
(`Sec-Fetch-Site: none` being the most-trusted value) **does not hold in
|
||||||
practice**.
|
practice**.
|
||||||
|
|
||||||
|
**Correction, 2026-10-18: `q` auto-submits again.**
|
||||||
|
- **Signed out:** in headless Firefox 157, `chatgpt.com/?q=` sent the prompt
|
||||||
|
and ChatGPT answered with no keypress. This happened even when an unrelated
|
||||||
|
page navigated to the URL (`Sec-Fetch-Site: cross-site`).
|
||||||
|
- **Maintainer's Firefox:** the same day, the maintainer reported that prompts
|
||||||
|
were sent without pressing Enter.
|
||||||
|
- **A week earlier:** on 2026-10-12 the signed-out path showed a verification
|
||||||
|
error and the signed-in path still needed Enter
|
||||||
|
(`docs/toolbar-verification.md`).
|
||||||
|
|
||||||
|
So this changed within a week and may differ between accounts. Treat both
|
||||||
|
behaviours as possible. The copy rule below still applies, and the README
|
||||||
|
already says ChatGPT decides whether a prompt is sent. Appendix B covers what
|
||||||
|
this means for selection smuggling.
|
||||||
|
|
||||||
No copy in the options panel, README, or AMO listing may say the query is
|
No copy in the options panel, README, or AMO listing may say the query is
|
||||||
*searched*, *sent*, or *asked* — only that ChatGPT opens **with the query filled
|
*searched*, *sent*, or *asked* — only that ChatGPT opens **with the query filled
|
||||||
in**. The two-keypress behaviour is an upstream ChatGPT change, not a defect in
|
in**. The two-keypress behaviour is an upstream ChatGPT change, not a defect in
|
||||||
@@ -698,7 +713,7 @@ by running commands against copies under `/tmp`; the repo itself was unmodified.
|
|||||||
| Parameter | Rating | Basis |
|
| Parameter | Rating | Basis |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `q` prefills | **Solid** | Dated 2026 sources; maintained tooling; OpenAI's own extension uses it; a live probe returned HTTP 200 with all parameters preserved. |
|
| `q` prefills | **Solid** | Dated 2026 sources; maintained tooling; OpenAI's own extension uses it; a live probe returned HTTP 200 with all parameters preserved. |
|
||||||
| `q` auto-submits | **Refuted, and confirmed by first-hand test** | Prefill-only in every dated report since 2025-11-11. Tenable TRA-2025-22 documents OpenAI gating auto-submit on `Sec-Fetch-Site`. Reproduced 2026-07-29 in the maintainer's logged-in Firefox via this extension's own search-provider path. |
|
| `q` auto-submits | **Varies; auto-submitting again as of 2026-10-18** | Prefill-only in every dated report from 2025-11-11, when Tenable TRA-2025-22 documented OpenAI gating auto-submit on `Sec-Fetch-Site`, and reproduced 2026-07-29 in the maintainer's logged-in Firefox. On 2026-10-18 it auto-submitted signed out, including after a cross-site navigation, and the maintainer reported the same in their own Firefox. See the correction under the `q` section. |
|
||||||
| `model` honoured alongside `q` | **Uncertain** | An unresolved 2024-12-28 report says `q` forces GPT-4o regardless; counter-evidence exists from 2024–2025; no 2026 source positively confirms. Maintained tooling grades it experimental. |
|
| `model` honoured alongside `q` | **Uncertain** | An unresolved 2024-12-28 report says `q` forces GPT-4o regardless; counter-evidence exists from 2024–2025; no 2026 source positively confirms. Maintained tooling grades it experimental. |
|
||||||
| Valid `model` slugs | **Uncertain** | No authoritative list; sightings rot (`gpt-4`, `o1-preview`, `gpt-4o`, `gpt-5-thinking`, `auto`, `instant`, `thinking`). |
|
| Valid `model` slugs | **Uncertain** | No authoritative list; sightings rot (`gpt-4`, `o1-preview`, `gpt-4o`, `gpt-5-thinking`, `auto`, `instant`, `thinking`). |
|
||||||
| `temporary-chat=true` | **Uncertain** | Community-confirmed originally; multiple reports of it not applying or reverting after the first message; OpenAI reworking the feature. |
|
| `temporary-chat=true` | **Uncertain** | Community-confirmed originally; multiple reports of it not applying or reverting after the first message; OpenAI reworking the feature. |
|
||||||
@@ -790,6 +805,16 @@ composer until a human presses Enter. **Auto-submit deletes that checkpoint.**
|
|||||||
This is also why the context menu and shortcut remain safe *as long as they only
|
This is also why the context menu and shortcut remain safe *as long as they only
|
||||||
prefill*.
|
prefill*.
|
||||||
|
|
||||||
|
> **2026-10-18:** ChatGPT itself now auto-submits `?q=` prompts, at least for
|
||||||
|
> some accounts (see the correction under the `q` section). While that holds,
|
||||||
|
> the composer checkpoint above is gone for the context menu, toolbar button and
|
||||||
|
> shortcut alike: hidden text in a selection can reach an answered prompt
|
||||||
|
> without review. The extension still adds no submission of its own, and this
|
||||||
|
> appendix's rejection stands. 1.2.0 ships with the behaviour known. Possible
|
||||||
|
> mitigations, such as framing the default Ask prompt as data the way the preset
|
||||||
|
> actions already do, or an option to review page-sourced text first, are open
|
||||||
|
> follow-ups rather than part of this design.
|
||||||
|
|
||||||
**`webNavigation.transitionType` is not a security boundary.** The signal looks
|
**`webNavigation.transitionType` is not a security boundary.** The signal looks
|
||||||
right — a `gpt` search emits `transitionType: "generated"` with
|
right — a `gpt` search emits `transitionType: "generated"` with
|
||||||
`transitionQualifiers: ["from_address_bar"]`, and content cannot forge those
|
`transitionQualifiers: ["from_address_bar"]`, and content cannot forge those
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
|
|
||||||
export const OPEN_IN_VALUES = ["new-tab", "background-tab", "new-window"];
|
export const OPEN_IN_VALUES = ["new-tab", "background-tab", "new-window"];
|
||||||
|
|
||||||
|
// In UTF-16 units, as maxlength counts. Keeps the template far inside
|
||||||
|
// storage.sync's 8192-byte item quota even when every character is a pasted
|
||||||
|
// control character (six bytes once JSON-escaped), and far beyond any template
|
||||||
|
// the 8,000-character URL budget could send.
|
||||||
|
export const MAX_TEMPLATE_CHARS = 1000;
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS = {
|
export const DEFAULT_SETTINGS = {
|
||||||
contextMenuMode: "simple",
|
contextMenuMode: "simple",
|
||||||
showTranslate: false,
|
showTranslate: false,
|
||||||
|
|||||||
+12
-1
@@ -25,12 +25,23 @@ export function prepareAction(request) {
|
|||||||
}
|
}
|
||||||
const resolved = { ...request, language };
|
const resolved = { ...request, language };
|
||||||
const serialize = text => serializePrompt(request.settings, renderPrompt(resolved, text));
|
const serialize = text => serializePrompt(request.settings, renderPrompt(resolved, text));
|
||||||
|
// Each code point adds at least one URL character, so none past the first
|
||||||
|
// MAX_URL_CHARS can fit. Capping first keeps a select-all on a huge page from
|
||||||
|
// costing seconds of re-encoding. A capped source is never sent as ready: its
|
||||||
|
// kept prefix may trim down to something that fits and silently drop the rest.
|
||||||
|
const points = [];
|
||||||
|
let capped = false;
|
||||||
|
for (const point of source) {
|
||||||
|
if (points.length === MAX_URL_CHARS) { capped = true; break; }
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
if (!capped) {
|
||||||
const url = serialize(source);
|
const url = serialize(source);
|
||||||
if (url.length <= MAX_URL_CHARS) return { kind: 'ready', url };
|
if (url.length <= MAX_URL_CHARS) return { kind: 'ready', url };
|
||||||
|
}
|
||||||
if (isUrl) return error('This URL is too long to send intact. No part of it has been sent.');
|
if (isUrl) return error('This URL is too long to send intact. No part of it has been sent.');
|
||||||
const marker = '\n[Selected text shortened]';
|
const marker = '\n[Selected text shortened]';
|
||||||
if (serialize(marker).length >= MAX_URL_CHARS) return error('The prompt instructions are too long. Shorten your prompt template and try again.');
|
if (serialize(marker).length >= MAX_URL_CHARS) return error('The prompt instructions are too long. Shorten your prompt template and try again.');
|
||||||
const points = Array.from(source);
|
|
||||||
let low = 0, high = points.length;
|
let low = 0, high = points.length;
|
||||||
while (low < high) {
|
while (low < high) {
|
||||||
const mid = Math.ceil((low + high) / 2);
|
const mid = Math.ceil((low + high) / 2);
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { DEFAULT_SETTINGS, OPEN_IN_VALUES } from "./defaults.js";
|
import { DEFAULT_SETTINGS, MAX_TEMPLATE_CHARS, OPEN_IN_VALUES } from "./defaults.js";
|
||||||
|
|
||||||
export function normalizeLanguage(value) {
|
export function normalizeLanguage(value) {
|
||||||
if (typeof value !== "string") return "";
|
if (typeof value !== "string") return "";
|
||||||
@@ -28,7 +28,7 @@ export function mergeSettings(stored) {
|
|||||||
if (OPEN_IN_VALUES.includes(stored.openIn)) {
|
if (OPEN_IN_VALUES.includes(stored.openIn)) {
|
||||||
merged.openIn = stored.openIn;
|
merged.openIn = stored.openIn;
|
||||||
}
|
}
|
||||||
if (typeof stored.promptTemplate === "string") {
|
if (typeof stored.promptTemplate === "string" && stored.promptTemplate.length <= MAX_TEMPLATE_CHARS) {
|
||||||
merged.promptTemplate = stored.promptTemplate;
|
merged.promptTemplate = stored.promptTemplate;
|
||||||
}
|
}
|
||||||
return merged;
|
return merged;
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Search with ChatGPT powered by OpenAI!",
|
"name": "Search with ChatGPT powered by OpenAI!",
|
||||||
"description": "Adds a ChatGPT search engine",
|
"description": "Adds a ChatGPT search engine, plus a right-click menu, toolbar button and shortcut that open ChatGPT with your selected text.",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"author": "Mathew Guest <mat@zavage.net>",
|
"author": "Mathew Guest <mat@zavage.net>",
|
||||||
"homepage_url": "https://zavage-software.com/portfolio/search-with-ChatGPT",
|
"homepage_url": "https://zavage-software.com/portfolio/search-with-ChatGPT",
|
||||||
"version": "1.1",
|
"version": "1.2",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/powered-by-openai-icon-16x16.png",
|
"16": "icons/powered-by-openai-icon-16x16.png",
|
||||||
"32": "icons/powered-by-openai-icon-32x32.png",
|
"32": "icons/powered-by-openai-icon-32x32.png",
|
||||||
|
|||||||
@@ -80,8 +80,8 @@
|
|||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="promptTemplate">Prompt template</label>
|
<label for="promptTemplate">Prompt template</label>
|
||||||
<input type="text" id="promptTemplate" spellcheck="false" />
|
<input type="text" id="promptTemplate" spellcheck="false" maxlength="1000" />
|
||||||
<p class="hint"><code>{query}</code> is replaced with your selected text. Used by Ask normally, Simple mode, the toolbar button, and the keyboard shortcut. Other advanced actions use their own prompts.</p>
|
<p class="hint"><code>{query}</code> is replaced with your selected text. Used by Ask normally, Simple mode, the toolbar button, and the keyboard shortcut. Other advanced actions use their own prompts. Up to 1,000 characters.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "search-with-chatgpt-powered-by-openai-extension",
|
"name": "search-with-chatgpt-powered-by-openai-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "search-with-chatgpt-powered-by-openai-extension",
|
"name": "search-with-chatgpt-powered-by-openai-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"web-ext": "10.5.0"
|
"web-ext": "10.5.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "search-with-chatgpt-powered-by-openai-extension",
|
"name": "search-with-chatgpt-powered-by-openai-extension",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Development tooling for the Search with ChatGPT Firefox extension.",
|
"description": "Development tooling for the Search with ChatGPT Firefox extension.",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { DEFAULT_SETTINGS } from '../lib/defaults.js';
|
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 { prepareAction } = await import('../lib/prepare-action.js').catch(()=>({}));
|
||||||
const req = (overrides={}) => ({actionId:'ask',source:'Hello',settings:{...DEFAULT_SETTINGS},...overrides});
|
const req = (overrides={}) => ({actionId:'ask',source:'Hello',settings:{...DEFAULT_SETTINGS},...overrides});
|
||||||
test('ready actions preserve source and hints, translation asks only when needed', () => {
|
test('ready actions preserve source and hints, translation asks only when needed', () => {
|
||||||
@@ -49,3 +50,22 @@ test('long selections preserve instructions, repetition and Unicode with explici
|
|||||||
const translated=prepareAction(req({actionId:'translate',language:'Spanish',source:'x'.repeat(20000)}));
|
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/);
|
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]'));
|
||||||
|
});
|
||||||
|
|||||||
+27
-1
@@ -1,7 +1,9 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
import { DEFAULT_SETTINGS } from "../lib/defaults.js";
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
import { DEFAULT_SETTINGS, MAX_TEMPLATE_CHARS } from "../lib/defaults.js";
|
||||||
import { mergeSettings } from "../lib/settings.js";
|
import { mergeSettings } from "../lib/settings.js";
|
||||||
|
|
||||||
test("empty object yields the defaults", () => {
|
test("empty object yields the defaults", () => {
|
||||||
@@ -67,6 +69,30 @@ test("wrong types fall back to the default", () => {
|
|||||||
assert.equal(merged.promptTemplate, DEFAULT_SETTINGS.promptTemplate);
|
assert.equal(merged.promptTemplate, DEFAULT_SETTINGS.promptTemplate);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a prompt template over the length limit falls back to the default", () => {
|
||||||
|
assert.equal(typeof MAX_TEMPLATE_CHARS, "number");
|
||||||
|
const atLimit = "x".repeat(MAX_TEMPLATE_CHARS);
|
||||||
|
assert.equal(mergeSettings({ promptTemplate: atLimit }).promptTemplate, atLimit);
|
||||||
|
assert.equal(mergeSettings({ promptTemplate: `${atLimit}x` }).promptTemplate,
|
||||||
|
DEFAULT_SETTINGS.promptTemplate);
|
||||||
|
});
|
||||||
|
|
||||||
|
// storage.sync rejects any item over 8192 bytes (key plus JSON value), and the
|
||||||
|
// options page writes every field in one set(), so one oversized template would
|
||||||
|
// make every later save fail. The worst character is a pasted control
|
||||||
|
// character, which JSON escapes to six bytes.
|
||||||
|
test("a template at the limit fits storage.sync's per-item quota in the worst case", () => {
|
||||||
|
const worst = "\u0001".repeat(MAX_TEMPLATE_CHARS);
|
||||||
|
const bytes = new TextEncoder().encode("promptTemplate" + JSON.stringify(worst)).length;
|
||||||
|
assert.ok(bytes <= 8192, `${bytes} bytes`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the options page enforces the same template limit", () => {
|
||||||
|
const html = readFileSync(new URL("../options/options.html", import.meta.url), "utf8");
|
||||||
|
const input = html.match(/<input[^>]*id="promptTemplate"[^>]*>/)?.[0] ?? "";
|
||||||
|
assert.equal(input.match(/maxlength="(\d+)"/)?.[1], String(MAX_TEMPLATE_CHARS));
|
||||||
|
});
|
||||||
|
|
||||||
test("an out-of-range openIn falls back to the default", () => {
|
test("an out-of-range openIn falls back to the default", () => {
|
||||||
assert.equal(mergeSettings({ openIn: "teleport" }).openIn, DEFAULT_SETTINGS.openIn);
|
assert.equal(mergeSettings({ openIn: "teleport" }).openIn, DEFAULT_SETTINGS.openIn);
|
||||||
assert.equal(mergeSettings({ openIn: "" }).openIn, DEFAULT_SETTINGS.openIn);
|
assert.equal(mergeSettings({ openIn: "" }).openIn, DEFAULT_SETTINGS.openIn);
|
||||||
|
|||||||
Reference in New Issue
Block a user