From bed36ca4add51662daa55bc754e46472e86fbfe8 Mon Sep 17 00:00:00 2001 From: Cole Medin Date: Thu, 16 Apr 2026 18:32:06 -0500 Subject: [PATCH] fix(workflows): add word boundary to context variable substitution regex (#1256) * fix(workflows): add word boundary to context variable substitution regex (#1112) Variable substitution for $CONTEXT, $EXTERNAL_CONTEXT, and $ISSUE_CONTEXT was matching as a prefix of longer identifiers like $CONTEXT_FILE, silently corrupting bash node scripts. Added negative lookahead (?![A-Za-z0-9_]) to CONTEXT_VAR_PATTERN_STR so only exact variable names are substituted. Changes: - Add negative lookahead to CONTEXT_VAR_PATTERN_STR regex in executor-shared.ts - Add regression test for prefix-match boundary case Fixes #1112 Co-Authored-By: Claude Opus 4.6 (1M context) * test(workflows): add missing boundary cases for context variable substitution Add three new test cases that complete coverage of the word-boundary fix from #1112: $ISSUE_CONTEXT with suffix variants, $ISSUE_CONTEXT with multiple suffixes, and contextSubstituted=false for suffix-only prompts. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .../workflows/src/executor-shared.test.ts | 44 +++++++++++++++++++ packages/workflows/src/executor-shared.ts | 3 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/workflows/src/executor-shared.test.ts b/packages/workflows/src/executor-shared.test.ts index 84346f13..80915621 100644 --- a/packages/workflows/src/executor-shared.test.ts +++ b/packages/workflows/src/executor-shared.test.ts @@ -167,6 +167,50 @@ describe('substituteWorkflowVariables', () => { expect(prompt).toBe('Issue: context-data. External: context-data'); }); + it('does not treat context variables as prefixes of longer identifiers', () => { + const { prompt, contextSubstituted } = substituteWorkflowVariables( + 'Context: $CONTEXT. File: $CONTEXT_FILE. External path: $EXTERNAL_CONTEXT_PATH. IssueId: $ISSUE_CONTEXT_ID', + 'run-1', + 'msg', + '/tmp', + 'main', + 'docs/', + 'context-data' + ); + expect(prompt).toBe( + 'Context: context-data. File: $CONTEXT_FILE. External path: $EXTERNAL_CONTEXT_PATH. IssueId: $ISSUE_CONTEXT_ID' + ); + expect(contextSubstituted).toBe(true); + }); + + it('does not substitute $ISSUE_CONTEXT when followed by identifier characters', () => { + const { prompt } = substituteWorkflowVariables( + 'Issue: $ISSUE_CONTEXT. ID: $ISSUE_CONTEXT_ID. Type: $ISSUE_CONTEXT_TYPE', + 'run-1', + 'msg', + '/tmp', + 'main', + 'docs/', + 'context-data' + ); + expect(prompt).toBe('Issue: context-data. ID: $ISSUE_CONTEXT_ID. Type: $ISSUE_CONTEXT_TYPE'); + }); + + it('does not set contextSubstituted when only suffix-extended context vars are present', () => { + const { prompt, contextSubstituted } = substituteWorkflowVariables( + 'Path: $CONTEXT_FILE', + 'run-1', + 'msg', + '/tmp', + 'main', + 'docs/', + 'context-data' + ); + // $CONTEXT_FILE is not a context variable — should be left untouched + expect(prompt).toBe('Path: $CONTEXT_FILE'); + expect(contextSubstituted).toBe(false); + }); + it('clears context variables when issueContext is undefined', () => { const { prompt, contextSubstituted } = substituteWorkflowVariables( 'Context: $CONTEXT here', diff --git a/packages/workflows/src/executor-shared.ts b/packages/workflows/src/executor-shared.ts index e1978ae1..255895a5 100644 --- a/packages/workflows/src/executor-shared.ts +++ b/packages/workflows/src/executor-shared.ts @@ -242,7 +242,8 @@ export async function loadCommandPrompt( // ─── Variable Substitution ─────────────────────────────────────────────────── /** Pattern string for context variables - used to create fresh regex instances */ -export const CONTEXT_VAR_PATTERN_STR = '\\$(?:CONTEXT|EXTERNAL_CONTEXT|ISSUE_CONTEXT)'; +export const CONTEXT_VAR_PATTERN_STR = + '\\$(?:CONTEXT|EXTERNAL_CONTEXT|ISSUE_CONTEXT)(?![A-Za-z0-9_])'; /** * Substitute workflow variables in a prompt.