Skip to content

Commit ce47c11

Browse files
CopilotPureWeen
andcommitted
fix: Address PR review - robust YAML parsing, sanitized output, test isolation
1. ParsePromptFile now requires closing --- at start of a line, preventing dashes inside YAML values from being treated as frontmatter delimiters 2. SavePrompt quotes name/description in YAML and strips newlines/escapes quotes via new SanitizeYamlValue helper 3. DiscoverPrompts tests filter by Source==Project to avoid interference from real ~/.polypilot/prompts/ contents 4. Added 4 new tests for dashes-in-values, YAML sanitization Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
1 parent 97c1fa6 commit ce47c11

2 files changed

Lines changed: 75 additions & 8 deletions

File tree

PolyPilot.Tests/PromptLibraryTests.cs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ public void DiscoverPrompts_FromProjectDirectories()
143143
Directory.CreateDirectory(promptDir);
144144
File.WriteAllText(Path.Combine(promptDir, "deploy.md"), "---\nname: Deploy\ndescription: Deploy the app\n---\nDeploy steps...");
145145

146-
var prompts = PromptLibraryService.DiscoverPrompts(projectDir);
146+
var prompts = PromptLibraryService.DiscoverPrompts(projectDir)
147+
.Where(p => p.Source == PromptSource.Project).ToList();
147148

148149
Assert.Single(prompts);
149150
Assert.Equal("Deploy", prompts[0].Name);
@@ -158,7 +159,8 @@ public void DiscoverPrompts_CopilotPromptsDir()
158159
Directory.CreateDirectory(promptDir);
159160
File.WriteAllText(Path.Combine(promptDir, "review.md"), "Review code carefully.");
160161

161-
var prompts = PromptLibraryService.DiscoverPrompts(projectDir);
162+
var prompts = PromptLibraryService.DiscoverPrompts(projectDir)
163+
.Where(p => p.Source == PromptSource.Project).ToList();
162164

163165
Assert.Single(prompts);
164166
Assert.Equal("review", prompts[0].Name);
@@ -176,7 +178,8 @@ public void DiscoverPrompts_MultipleProjectDirs()
176178
File.WriteAllText(Path.Combine(githubDir, "from-github.md"), "---\nname: GitHub Prompt\n---\nFrom github");
177179
File.WriteAllText(Path.Combine(copilotDir, "from-copilot.md"), "---\nname: Copilot Prompt\n---\nFrom copilot");
178180

179-
var prompts = PromptLibraryService.DiscoverPrompts(projectDir);
181+
var prompts = PromptLibraryService.DiscoverPrompts(projectDir)
182+
.Where(p => p.Source == PromptSource.Project).ToList();
180183

181184
Assert.Equal(2, prompts.Count);
182185
Assert.Contains(prompts, p => p.Name == "GitHub Prompt");
@@ -272,12 +275,47 @@ public void DiscoverPrompts_ClaudePromptsDir()
272275
Directory.CreateDirectory(promptDir);
273276
File.WriteAllText(Path.Combine(promptDir, "analyze.md"), "---\nname: Analyze\n---\nAnalyze the code.");
274277

275-
var prompts = PromptLibraryService.DiscoverPrompts(projectDir);
278+
var prompts = PromptLibraryService.DiscoverPrompts(projectDir)
279+
.Where(p => p.Source == PromptSource.Project).ToList();
276280

277281
Assert.Single(prompts);
278282
Assert.Equal("Analyze", prompts[0].Name);
279283
}
280284

285+
[Fact]
286+
public void ParsePromptFile_DashesInsideYamlValue_NotTreatedAsClosing()
287+
{
288+
var content = "---\nname: test---name\ndescription: a---b\n---\nBody here";
289+
var filePath = "/test.md";
290+
291+
var (name, description, body) = PromptLibraryService.ParsePromptFile(content, filePath);
292+
293+
Assert.Equal("test---name", name);
294+
Assert.Equal("a---b", description);
295+
Assert.Equal("Body here", body);
296+
}
297+
298+
[Fact]
299+
public void SanitizeYamlValue_StripsNewlines()
300+
{
301+
var result = PromptLibraryService.SanitizeYamlValue("line1\nline2\r\nline3");
302+
Assert.Equal("line1 line2 line3", result);
303+
}
304+
305+
[Fact]
306+
public void SanitizeYamlValue_EscapesQuotes()
307+
{
308+
var result = PromptLibraryService.SanitizeYamlValue("say \"hello\"");
309+
Assert.Equal("say \\\"hello\\\"", result);
310+
}
311+
312+
[Fact]
313+
public void SanitizeYamlValue_PlainString_Unchanged()
314+
{
315+
var result = PromptLibraryService.SanitizeYamlValue("simple name");
316+
Assert.Equal("simple name", result);
317+
}
318+
281319
public void Dispose()
282320
{
283321
try { Directory.Delete(_testDir, true); } catch { }

PolyPilot/Services/PromptLibraryService.cs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,20 @@ internal static (string name, string description, string body) ParsePromptFile(s
122122

123123
if (content.StartsWith("---"))
124124
{
125-
var endIdx = content.IndexOf("---", 3, StringComparison.Ordinal);
125+
// Search for closing --- that starts on its own line
126+
var endIdx = -1;
127+
var searchFrom = 3;
128+
while (searchFrom < content.Length)
129+
{
130+
var idx = content.IndexOf("---", searchFrom, StringComparison.Ordinal);
131+
if (idx < 0) break;
132+
if (idx == 0 || content[idx - 1] == '\n')
133+
{
134+
endIdx = idx;
135+
break;
136+
}
137+
searchFrom = idx + 1;
138+
}
126139
if (endIdx > 0)
127140
{
128141
var frontmatter = content[3..endIdx];
@@ -159,14 +172,18 @@ public static SavedPrompt SavePrompt(string name, string content, string? descri
159172
var safeName = SanitizeFileName(name);
160173
var filePath = Path.Combine(UserPromptsDir, safeName + ".md");
161174

175+
// Sanitize name/description to prevent YAML corruption
176+
var yamlName = SanitizeYamlValue(name);
177+
var yamlDesc = description != null ? SanitizeYamlValue(description) : null;
178+
162179
var fileContent = "";
163-
if (!string.IsNullOrWhiteSpace(description))
180+
if (!string.IsNullOrWhiteSpace(yamlDesc))
164181
{
165-
fileContent = $"---\nname: {name}\ndescription: {description}\n---\n{content}";
182+
fileContent = $"---\nname: \"{yamlName}\"\ndescription: \"{yamlDesc}\"\n---\n{content}";
166183
}
167184
else
168185
{
169-
fileContent = $"---\nname: {name}\n---\n{content}";
186+
fileContent = $"---\nname: \"{yamlName}\"\n---\n{content}";
170187
}
171188

172189
File.WriteAllText(filePath, fileContent);
@@ -216,6 +233,18 @@ public static bool DeletePrompt(string name)
216233
.FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
217234
}
218235

236+
/// <summary>
237+
/// Sanitize a string for safe inclusion in a YAML value.
238+
/// Strips newlines and escapes double quotes.
239+
/// </summary>
240+
internal static string SanitizeYamlValue(string value)
241+
{
242+
return value
243+
.Replace("\r", "")
244+
.Replace("\n", " ")
245+
.Replace("\"", "\\\"");
246+
}
247+
219248
/// <summary>
220249
/// Sanitize a name into a safe filename (alphanumeric, hyphens, underscores).
221250
/// </summary>

0 commit comments

Comments
 (0)