Skip to content

Commit 066db72

Browse files
committed
test: add path corruption prevention tests
Adds 14 tests to prevent regressions of path corruption bugs: Backslash Preservation Tests: - CommandParser preserves backslashes in Windows paths - Handles quoted paths with spaces correctly - Distinguishes between escape sequences (\ \") and literals (\n) - Tests the exact corruption scenario from bug report Trailing Separator Tests: - ArgumentGraph adds trailing separators to all normalized paths - Deduplicates paths regardless of trailing separator state - Prevents duplicates like "C:\Users" vs "C:\Users\" - Verifies 3 different path formats merge correctly These tests ensure the fixes in commits 01842ed (backslash preservation) and ace7e75 (trailing separators) remain in place. 🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent cbab7af commit 066db72

File tree

1 file changed

+274
-0
lines changed

1 file changed

+274
-0
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
using System;
2+
using System.IO;
3+
using Xunit;
4+
5+
namespace PSCue.Module.Tests;
6+
7+
/// <summary>
8+
/// Tests to prevent path corruption issues that caused duplicate entries and missing backslashes.
9+
/// These tests ensure the fixes for backslash preservation and trailing separator consistency remain in place.
10+
/// </summary>
11+
public class PathCorruptionTests
12+
{
13+
[Theory]
14+
[InlineData(@"D:\source\datadog\dd-trace-dotnet")]
15+
[InlineData(@"C:\Users\Test\Documents")]
16+
public void CommandParser_PreservesBackslashesInWindowsPaths(string windowsPath)
17+
{
18+
// Arrange
19+
var parser = new CommandParser();
20+
var commandLine = $"cd {windowsPath}";
21+
22+
// Act
23+
var result = parser.Parse(commandLine);
24+
25+
// Assert
26+
Assert.Equal("cd", result.Command);
27+
Assert.Single(result.Arguments);
28+
29+
// The path should still contain all backslashes
30+
var parsedPath = result.Arguments[0].Text;
31+
Assert.Equal(windowsPath, parsedPath);
32+
33+
// Count backslashes - they should match
34+
var originalBackslashCount = windowsPath.Count(c => c == '\\');
35+
var parsedBackslashCount = parsedPath.Count(c => c == '\\');
36+
Assert.Equal(originalBackslashCount, parsedBackslashCount);
37+
}
38+
39+
[Fact]
40+
public void CommandParser_PreservesBackslashesInPathsWithSpaces()
41+
{
42+
// Arrange
43+
var parser = new CommandParser();
44+
var windowsPath = @"C:\Program Files\App";
45+
var commandLine = $"cd \"{windowsPath}\"";
46+
47+
// Act
48+
var result = parser.Parse(commandLine);
49+
50+
// Assert
51+
Assert.Equal("cd", result.Command);
52+
Assert.Single(result.Arguments);
53+
54+
// The path should still contain all backslashes
55+
var parsedPath = result.Arguments[0].Text;
56+
Assert.Equal(windowsPath, parsedPath);
57+
Assert.Contains(@"\Program Files\", parsedPath);
58+
}
59+
60+
[Fact]
61+
public void CommandParser_PreservesBackslashesInQuotedPaths()
62+
{
63+
// Arrange
64+
var parser = new CommandParser();
65+
var windowsPath = @"D:\source\my folder\project";
66+
var commandLine = $"cd \"{windowsPath}\"";
67+
68+
// Act
69+
var result = parser.Parse(commandLine);
70+
71+
// Assert
72+
var parsedPath = result.Arguments[0].Text;
73+
Assert.Equal(windowsPath, parsedPath);
74+
Assert.Contains(@"\source\", parsedPath);
75+
Assert.Contains(@"\my folder\", parsedPath);
76+
}
77+
78+
[Fact]
79+
public void CommandParser_HandlesEscapedBackslash()
80+
{
81+
// Arrange
82+
var parser = new CommandParser();
83+
parser.RegisterParameterRequiringValue("-m");
84+
var commandLine = @"git commit -m ""test \\ message""";
85+
86+
// Act
87+
var result = parser.Parse(commandLine);
88+
89+
// Assert
90+
Assert.Equal(3, result.Arguments.Count);
91+
// Escaped backslash (\\) should become single backslash
92+
Assert.Equal(@"test \ message", result.Arguments[2].Text);
93+
}
94+
95+
[Fact]
96+
public void CommandParser_DoesNotTreatSingleBackslashAsEscape()
97+
{
98+
// Arrange
99+
var parser = new CommandParser();
100+
parser.RegisterParameterRequiringValue("-m");
101+
var commandLine = @"git commit -m ""test\nmessage""";
102+
103+
// Act
104+
var result = parser.Parse(commandLine);
105+
106+
// Assert
107+
Assert.Equal(3, result.Arguments.Count);
108+
// \n is not a recognized escape sequence, both characters should be preserved
109+
Assert.Equal(@"test\nmessage", result.Arguments[2].Text);
110+
}
111+
112+
[Fact]
113+
public void ArgumentGraph_NormalizedPaths_HaveTrailingSeparator()
114+
{
115+
// Arrange
116+
var graph = new ArgumentGraph();
117+
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
118+
119+
// Act - record the same directory multiple times with different trailing separator states
120+
graph.RecordUsage("cd", new[] { tempDir }, workingDirectory: tempDir);
121+
graph.RecordUsage("cd", new[] { tempDir + Path.DirectorySeparatorChar }, workingDirectory: tempDir);
122+
graph.RecordUsage("cd", new[] { tempDir + Path.DirectorySeparatorChar + Path.DirectorySeparatorChar }, workingDirectory: tempDir);
123+
124+
// Assert - should be deduplicated to a single entry
125+
var knowledge = graph.GetCommandKnowledge("cd");
126+
Assert.NotNull(knowledge);
127+
Assert.Single(knowledge.Arguments);
128+
129+
// The stored path must have exactly one trailing separator
130+
var storedPath = knowledge.Arguments.Keys.First();
131+
Assert.True(
132+
storedPath.EndsWith(Path.DirectorySeparatorChar) || storedPath.EndsWith(Path.AltDirectorySeparatorChar),
133+
$"Normalized path '{storedPath}' should end with directory separator"
134+
);
135+
136+
// Should not have double separators at the end
137+
var separatorString = Path.DirectorySeparatorChar.ToString();
138+
Assert.False(
139+
storedPath.EndsWith(separatorString + separatorString),
140+
$"Normalized path '{storedPath}' should not have double trailing separators"
141+
);
142+
143+
// All three calls should have been merged
144+
Assert.Equal(3, knowledge.Arguments.Values.First().UsageCount);
145+
}
146+
147+
[Fact]
148+
public void ArgumentGraph_DifferentPathFormats_DeduplicateCorrectly()
149+
{
150+
// Arrange
151+
var graph = new ArgumentGraph();
152+
var tempDir = Path.GetTempPath();
153+
var targetDir = Path.Combine(tempDir, "test-dedup");
154+
Directory.CreateDirectory(targetDir);
155+
156+
try
157+
{
158+
// Act - record the same directory via different path formats
159+
var absolutePath = Path.GetFullPath(targetDir);
160+
var absoluteWithTrailing = absolutePath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
161+
var relativePath = "test-dedup";
162+
163+
graph.RecordUsage("cd", new[] { absolutePath }, workingDirectory: tempDir);
164+
graph.RecordUsage("cd", new[] { absoluteWithTrailing }, workingDirectory: tempDir);
165+
graph.RecordUsage("cd", new[] { relativePath }, workingDirectory: tempDir);
166+
167+
// Assert - all should resolve to the same normalized path
168+
var knowledge = graph.GetCommandKnowledge("cd");
169+
Assert.NotNull(knowledge);
170+
Assert.Single(knowledge.Arguments);
171+
172+
// Usage count should be 3 (all merged)
173+
var stats = knowledge.Arguments.Values.First();
174+
Assert.Equal(3, stats.UsageCount);
175+
176+
// The normalized path should have trailing separator
177+
var normalizedPath = knowledge.Arguments.Keys.First();
178+
Assert.True(
179+
normalizedPath.EndsWith(Path.DirectorySeparatorChar) || normalizedPath.EndsWith(Path.AltDirectorySeparatorChar),
180+
$"Normalized path '{normalizedPath}' should have trailing separator"
181+
);
182+
}
183+
finally
184+
{
185+
try { Directory.Delete(targetDir, recursive: true); } catch { }
186+
}
187+
}
188+
189+
[Fact]
190+
public void ArgumentGraph_WithoutWorkingDirectory_SkipsNormalization()
191+
{
192+
// Arrange
193+
var graph = new ArgumentGraph();
194+
var path = @"D:\source\test";
195+
196+
// Act - record without working directory (should skip normalization)
197+
graph.RecordUsage("cd", new[] { path }, workingDirectory: null);
198+
199+
// Assert - path should be stored as-is (not normalized)
200+
var knowledge = graph.GetCommandKnowledge("cd");
201+
Assert.NotNull(knowledge);
202+
203+
// When working directory is null, paths are stored without normalization
204+
// This is expected behavior per CLAUDE.md documentation
205+
var storedPath = knowledge.Arguments.Keys.First();
206+
Assert.Equal(path, storedPath);
207+
}
208+
209+
[Fact]
210+
public void CommandParser_ComplexWindowsPath_PreservesStructure()
211+
{
212+
// Arrange - simulate the exact corruption scenario from the bug report
213+
var parser = new CommandParser();
214+
var originalPath = @"D:\source\datadog\dd-trace-dotnet-APMSVLS-58";
215+
var commandLine = $"cd {originalPath}";
216+
217+
// Act
218+
var result = parser.Parse(commandLine);
219+
220+
// Assert
221+
var parsedPath = result.Arguments[0].Text;
222+
223+
// Path should be exactly as input - no corruption
224+
Assert.Equal(originalPath, parsedPath);
225+
226+
// Verify specific structure is preserved
227+
Assert.StartsWith(@"D:\", parsedPath);
228+
Assert.Contains(@"\source\", parsedPath);
229+
Assert.Contains(@"\datadog\", parsedPath);
230+
Assert.EndsWith("dd-trace-dotnet-APMSVLS-58", parsedPath);
231+
232+
// Should NOT be corrupted like: "D:sourcedatadogdd-trace-dotnet-APMSVLS-58"
233+
Assert.DoesNotContain("D:source", parsedPath);
234+
Assert.DoesNotContain("datadogdd-trace", parsedPath);
235+
}
236+
237+
[Fact]
238+
public void CommandParser_EscapedQuotes_PreservesQuotesNotBackslashes()
239+
{
240+
// Arrange
241+
var parser = new CommandParser();
242+
parser.RegisterParameterRequiringValue("-m");
243+
var commandLine = @"git commit -m ""test \""quoted\"" message""";
244+
245+
// Act
246+
var result = parser.Parse(commandLine);
247+
248+
// Assert
249+
Assert.Equal(3, result.Arguments.Count);
250+
// Escaped quotes should be preserved, backslashes should be consumed
251+
Assert.Equal(@"test ""quoted"" message", result.Arguments[2].Text);
252+
}
253+
254+
[Theory]
255+
[InlineData(@"C:\Users")]
256+
[InlineData(@"D:\source\")]
257+
[InlineData(@"E:\projects\app")]
258+
public void ArgumentGraph_AlwaysNormalizesWithTrailingSeparator(string inputPath)
259+
{
260+
// Arrange
261+
var graph = new ArgumentGraph();
262+
var workingDir = Path.GetTempPath();
263+
264+
// Act
265+
graph.RecordUsage("cd", new[] { inputPath }, workingDirectory: workingDir);
266+
267+
// Assert
268+
var knowledge = graph.GetCommandKnowledge("cd");
269+
Assert.NotNull(knowledge);
270+
271+
var storedPath = knowledge.Arguments.Keys.First();
272+
Assert.EndsWith(Path.DirectorySeparatorChar.ToString(), storedPath);
273+
}
274+
}

0 commit comments

Comments
 (0)