Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/core/project/bedrockAgentImport/baseTranslator.escape.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, test } from "bun:test";
import { escapePythonTripleQuoted } from "./baseTranslator";

// escapePythonTripleQuoted escapes a value for embedding inside a Python
// triple-double-quoted literal: SYSTEM_PROMPT = """<escaped>""". For the result
// to be a valid literal, no double-quote in the value may abut the closing
// delimiter — a trailing `"` (or `""`) merges with `"""` into `""""`, which
// Python reads as a closed string plus a dangling quote (a syntax error).
describe("escapePythonTripleQuoted", () => {
test("escapes a trailing double-quote so it does not merge with the closing delimiter", () => {
const escaped = escapePythonTripleQuoted('Always answer in "quotes"');
// Embedded as """<escaped>""", a trailing unescaped quote merges with the
// closing delimiter into """" and breaks the literal; the quote must be escaped.
expect(escaped).toBe('Always answer in \\"quotes\\"');
});

test("still escapes an interior triple-quote run", () => {
expect(escapePythonTripleQuoted('emit """ here')).toBe('emit \\"\\"\\" here');
});
});
6 changes: 5 additions & 1 deletion src/core/project/bedrockAgentImport/baseTranslator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,11 @@ export function escapePythonString(value: string): string {
}

export function escapePythonTripleQuoted(value: string): string {
return value.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"');
// Escape every double-quote, not just """ runs. A quote left adjacent to the
// closing """ delimiter (a trailing " or "") merges into """" and breaks the
// literal; escaping each quote keeps it inside the string and also collapses
// any longer run safely.
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}

function pythonParameter(name: string, parameter: ImportedFunctionParameter): string {
Expand Down
9 changes: 9 additions & 0 deletions src/core/project/templates/renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,13 @@ describe("HandlebarsTemplateRenderer helpers", () => {
test("safeJson keeps double quotes for JSON-style output", () => {
expect(renderer.render("{{safeJson v}}", { v: "a" })).toBe('"a"');
});

// escapePyStr feeds a Python triple-double-quoted literal: """{{escapePyStr v}}""".
// A double-quote left adjacent to the closing """ delimiter merges into """"
// and breaks the literal, so every quote must be escaped.
test("escapePyStr escapes a trailing double-quote so it cannot merge with the closing delimiter", () => {
expect(renderer.render("{{escapePyStr v}}", { v: 'answer in "quotes"' })).toBe(
'answer in \\"quotes\\"',
);
});
});
5 changes: 4 additions & 1 deletion src/core/project/templates/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ export class HandlebarsTemplateRenderer implements TemplateRenderer {
);
this.hbs.registerHelper("escapePyStr", (value: unknown) => {
const str = typeof value === "string" ? value : "";
return new Handlebars.SafeString(str.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"'));
// Escape every double-quote, not just """ runs. A quote left adjacent to
// the closing """ delimiter (a trailing " or "") merges into """" and
// breaks the literal; escaping each quote keeps it inside the string.
return new Handlebars.SafeString(str.replace(/\\/g, "\\\\").replace(/"/g, '\\"'));
});
}

Expand Down
Loading