diff --git a/apps/server/src/scheduledTasks/webhookTemplate.test.ts b/apps/server/src/scheduledTasks/webhookTemplate.test.ts index 8b6f11e91f9f020bc3f9f7e3d38cd136899e54fb..f86a433c92e501d7e016b9f03f54355ac22a468c 100644 --- a/apps/server/src/scheduledTasks/webhookTemplate.test.ts +++ b/apps/server/src/scheduledTasks/webhookTemplate.test.ts @@ -52,6 +52,25 @@ describe("renderWebhookPrompt", () => { assert.deepEqual(rendered.missing, ["body.nope", "headers.x-missing", "unknown"]); }); + it("trims placeholder whitespace before resolving and reporting missing fields", () => { + const rendered = renderWebhookPrompt( + "{{\tbody.action\n}}|{{ body.nope }}|{{\nbody.nope\t}}|{{ \t\n }}", + githubPullRequest, + ); + assert.equal(rendered.prompt, "opened|||"); + assert.deepEqual(rendered.missing, ["body.nope", ""]); + }); + + it("preserves unclosed whitespace-heavy placeholders and resolves later valid ones", () => { + const unfinished = `Notify {{${" ".repeat(1024)}body`; + const rendered = renderWebhookPrompt( + `${unfinished}\n{{body.pull_request.number}}`, + githubPullRequest, + ); + assert.equal(rendered.prompt, `${unfinished}\n45`); + assert.deepEqual(rendered.missing, []); + }); + it("addresses form-encoded bodies and leaves other bodies to {{body}}", () => { const form = renderWebhookPrompt("{{body.text}} by {{body.user_name}}", { ...githubPullRequest, diff --git a/apps/server/src/scheduledTasks/webhookTemplate.ts b/apps/server/src/scheduledTasks/webhookTemplate.ts index 74c0f08ae7781dab7fb4f886c897988bd53f1b30..a0540e4a1766a1ee36dbbb44e8cf34a60ac6ac96 100644 --- a/apps/server/src/scheduledTasks/webhookTemplate.ts +++ b/apps/server/src/scheduledTasks/webhookTemplate.ts @@ -69,7 +69,8 @@ export function redactQuery(query: string): string { .join("&"); } -const PLACEHOLDER = /\{\{\s*([^{}]*?)\s*\}\}/g; +// Trim after matching so unclosed placeholders cannot backtrack over whitespace partitions. +const PLACEHOLDER = /\{\{([^{}]*)\}\}/g; function parseBody(request: WebhookRequest): unknown { const contentType = (request.headers["content-type"] ?? "").toLowerCase(); @@ -144,7 +145,8 @@ export function renderWebhookPrompt( } }; - const prompt = template.replace(PLACEHOLDER, (_match, expression: string) => { + const prompt = template.replace(PLACEHOLDER, (_match, rawExpression: string) => { + const expression = rawExpression.trim(); const value = resolve(expression); if (value === undefined || value === null) { if (!missing.includes(expression)) missing.push(expression);