Skip to content
Closed
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
4 changes: 4 additions & 0 deletions .changeset/patch-fix-wiki-note-placeholder.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions actions/setup/sh/validate_prompt_placeholders.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ fi

echo "🔍 Validating prompt placeholders..."

# Fallback: substitute __GH_AW_WIKI_NOTE__ with an empty string if still present.
# This handles workflows compiled before GH_AW_WIKI_NOTE was added to the substitution
# step for non-wiki repos. The wiki note is always optional (empty = no wiki note),
# so it is safe to clear it here rather than failing with a confusing placeholder error.
# If you see this message, run `gh aw update` to recompile your workflow and avoid the fallback.
if grep -q "__GH_AW_WIKI_NOTE__" "$PROMPT_FILE"; then
echo "⚠️ Warning: __GH_AW_WIKI_NOTE__ was not substituted by the substitution step."
echo " Applying fallback: replacing with empty string."
echo " To resolve this permanently, run 'gh aw update' to recompile your workflow."
sed 's/__GH_AW_WIKI_NOTE__//g' "$PROMPT_FILE" > "$PROMPT_FILE.tmp" && mv "$PROMPT_FILE.tmp" "$PROMPT_FILE"
fi

# Check for unreplaced environment variable placeholders (format: __GH_AW_*__)
if grep -q "__GH_AW_" "$PROMPT_FILE"; then
echo "❌ Error: Found unreplaced placeholders in prompt file:"
Expand Down
56 changes: 56 additions & 0 deletions actions/setup/sh/validate_prompt_placeholders_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,59 @@ fi
echo ""

echo "🎉 All validation tests passed!"

# Test 5: Prompt with __GH_AW_WIKI_NOTE__ placeholder (fallback should apply, not fail)
echo "Test 5: Prompt with unsubstituted __GH_AW_WIKI_NOTE__ (fallback should apply)"
cat > "$TEST_DIR/prompt_wiki_note.txt" << 'EOF'
<system>
# Repo Memory
You have access to a persistent repo memory folder at `/tmp/gh-aw/repo-memory/default/`
where you can read and write files that are stored in a git branch.__GH_AW_WIKI_NOTE__
</system>

# User Task
Do something useful.
EOF

export GH_AW_PROMPT="$TEST_DIR/prompt_wiki_note.txt"
OUTPUT=$(bash "$SCRIPT_PATH" 2>&1)
STATUS=$?
if [ $STATUS -eq 0 ]; then
echo "✅ Test 5 passed: Prompt with __GH_AW_WIKI_NOTE__ handled by fallback"
if echo "$OUTPUT" | grep -q "Warning.*__GH_AW_WIKI_NOTE__"; then
echo " (fallback warning message shown as expected)"
fi
# Verify __GH_AW_WIKI_NOTE__ was actually removed from the file
if grep -q "__GH_AW_WIKI_NOTE__" "$TEST_DIR/prompt_wiki_note.txt"; then
echo "❌ Test 5 failed: __GH_AW_WIKI_NOTE__ was not removed by fallback"
exit 1
fi
else
echo "❌ Test 5 failed: Prompt with __GH_AW_WIKI_NOTE__ caused unexpected failure"
echo "Output: $OUTPUT"
exit 1
fi
echo ""

# Test 6: Prompt with OTHER unreplaced placeholder (should still fail)
echo "Test 6: Prompt with other unreplaced placeholder (should still fail)"
cat > "$TEST_DIR/prompt_other_placeholder.txt" << 'EOF'
<system>
# System Instructions
Memory directory: __GH_AW_MEMORY_DIR__
</system>

# User Task
Do something useful.
EOF

export GH_AW_PROMPT="$TEST_DIR/prompt_other_placeholder.txt"
if bash "$SCRIPT_PATH" 2>&1; then
echo "❌ Test 6 failed: Prompt with __GH_AW_MEMORY_DIR__ was accepted"
exit 1
else
echo "✅ Test 6 passed: Prompt with other unreplaced placeholder rejected"
fi
echo ""

echo "🎉 All validation tests passed!"
70 changes: 70 additions & 0 deletions pkg/workflow/unified_prompt_creation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,76 @@ func TestGenerateUnifiedPromptCreationStep_CacheAndRepoMemory(t *testing.T) {
assert.Less(t, systemClosePos, userPos, "User task should be after system tag closes")
}

// TestGenerateUnifiedPromptCreationStep_WikiNoteInSubstitution verifies that GH_AW_WIKI_NOTE
// is always present in the placeholder substitution step for repo-memory, regardless of wiki mode.
// Regression test for: __GH_AW_WIKI_NOTE__ placeholder not substituted when Wiki is disabled.
func TestGenerateUnifiedPromptCreationStep_WikiNoteInSubstitution(t *testing.T) {
tests := []struct {
name string
wiki bool
expectEmpty bool
expectContain string
}{
{
name: "non-wiki mode has empty GH_AW_WIKI_NOTE in substitution",
wiki: false,
expectEmpty: true,
},
{
name: "wiki mode has non-empty GH_AW_WIKI_NOTE in substitution",
wiki: true,
expectEmpty: false,
expectContain: "GitHub Wiki",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
compiler := &Compiler{}
data := &WorkflowData{
ParsedTools: NewTools(map[string]any{}),
RepoMemoryConfig: &RepoMemoryConfig{
Memories: []RepoMemoryEntry{
{ID: "default", BranchName: "memory/default", Wiki: tt.wiki},
},
},
}

builtinSections := compiler.collectPromptSections(data)
userPromptChunks := []string{"# User Task"}

var yamlBuf strings.Builder
allExpressionMappings := compiler.generateUnifiedPromptCreationStep(&yamlBuf, builtinSections, userPromptChunks, nil, data)

require.NotEmpty(t, allExpressionMappings, "Expected non-empty expression mappings")

// Verify GH_AW_WIKI_NOTE is always in the substitution mappings
var wikiNoteMapping *ExpressionMapping
for _, m := range allExpressionMappings {
if m.EnvVar == "GH_AW_WIKI_NOTE" {
wikiNoteMapping = m
break
}
}
require.NotNil(t, wikiNoteMapping, "GH_AW_WIKI_NOTE must be present in substitution mappings (required to substitute __GH_AW_WIKI_NOTE__ in repo_memory_prompt.md)")

// Verify the substitution step YAML contains the env var
var substYaml strings.Builder
generatePlaceholderSubstitutionStep(&substYaml, allExpressionMappings, " ")
substOutput := substYaml.String()

assert.Contains(t, substOutput, "GH_AW_WIKI_NOTE:", "Substitution step must declare GH_AW_WIKI_NOTE env var")
assert.Contains(t, substOutput, "GH_AW_WIKI_NOTE: process.env.GH_AW_WIKI_NOTE", "Substitution step must pass GH_AW_WIKI_NOTE to substitutePlaceholders")

if tt.expectEmpty {
assert.Contains(t, substOutput, "GH_AW_WIKI_NOTE: ''", "Non-wiki mode should have empty GH_AW_WIKI_NOTE env var")
} else {
assert.Contains(t, substOutput, tt.expectContain, "Wiki mode should have wiki note content in GH_AW_WIKI_NOTE env var")
}
})
}
}

// TestGenerateUnifiedPromptCreationStep_PRContextConditional tests that PR context uses shell conditions
func TestGenerateUnifiedPromptCreationStep_PRContextConditional(t *testing.T) {
compiler := &Compiler{
Expand Down