fix: stop $ sequences in selected text corrupting the prompt

This commit is contained in:
golem 2026-08-01 23:15:39 -06:00
parent 1106f4e315
commit d7821e3dc7
2 changed files with 46 additions and 1 deletions

@ -21,7 +21,12 @@ function applyTemplate(template, query) {
}
const tpl = typeof template === "string" ? template : "";
if (tpl.includes("{query}")) {
return tpl.replaceAll("{query}", query);
// Replacer function, not a replacement string: String.prototype.replaceAll
// gives $&, $`, $', and $$ special meaning in a replacement STRING, and
// `query` is arbitrary page-selected text that can contain any of those
// (shell one-liners routinely do). The function form disables that
// interpretation entirely. Do not "simplify" this back to the string form.
return tpl.replaceAll("{query}", () => query);
}
const trimmed = tpl.trim();
return trimmed === "" ? query : `${trimmed} ${query}`;

@ -133,3 +133,43 @@ test("a query needing no truncation is left byte-identical", () => {
const raw = "a modest question about thermodynamics";
assert.equal(queryOf(buildChatGptUrl(DEFAULT_SETTINGS, raw)), raw);
});
// String.prototype.replaceAll gives $&, $`, $', and $$ special meaning in the
// REPLACEMENT string, and `query` (arbitrary page-selected text) is exactly
// that argument. These selections are ordinary shell one-liners, not
// adversarial input, and must round-trip byte-identically into `q`.
test("a selection containing $& survives the default {query} template byte-identically", () => {
const raw = "awk print $& here";
const url = buildChatGptUrl(DEFAULT_SETTINGS, raw);
assert.equal(queryOf(url), raw);
});
test("a selection containing $& survives a prefix template byte-identically", () => {
const raw = "awk print $& here";
const url = buildChatGptUrl(withSettings({ promptTemplate: "Explain: {query}" }), raw);
assert.equal(queryOf(url), `Explain: ${raw}`);
});
test("a selection containing $` and $' survives the default {query} template byte-identically", () => {
const raw = "shell $` and $' here";
const url = buildChatGptUrl(DEFAULT_SETTINGS, raw);
assert.equal(queryOf(url), raw);
});
test("a selection containing $` and $' survives a prefix template byte-identically", () => {
const raw = "shell $` and $' here";
const url = buildChatGptUrl(withSettings({ promptTemplate: "Explain: {query}" }), raw);
assert.equal(queryOf(url), `Explain: ${raw}`);
});
test("a selection containing $$ survives the default {query} template byte-identically", () => {
const raw = "PID is $$ in bash";
const url = buildChatGptUrl(DEFAULT_SETTINGS, raw);
assert.equal(queryOf(url), raw);
});
test("a selection containing $$ survives a prefix template byte-identically", () => {
const raw = "PID is $$ in bash";
const url = buildChatGptUrl(withSettings({ promptTemplate: "Explain: {query}" }), raw);
assert.equal(queryOf(url), `Explain: ${raw}`);
});