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) <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cole Medin 2026-04-16 18:32:06 -05:00 committed by GitHub
parent df828594d7
commit bed36ca4ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 46 additions and 1 deletions

View file

@ -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',

View file

@ -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.