Merge pull request 'fix: limit the prompt template to 1,000 characters' (#12) from fix/template-length-limit into develop

Reviewed-on: https://git.zavage.net/Zavage-Software/search-with-chatgpt-powered-by-openai-extension/pulls/12
This commit is contained in:
zavage
2026-10-18 02:26:43 -06:00
5 changed files with 38 additions and 5 deletions
+1
View File
@@ -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.
+6
View File
@@ -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,
+2 -2
View File
@@ -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
View File
@@ -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">
+27 -1
View File
@@ -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);