Skip to content

Commit 86c5591

Browse files
authored
Apply AI session prefix universally (custom names, directories, EF packages) (#959)
1 parent b235e06 commit 86c5591

10 files changed

Lines changed: 243 additions & 16 deletions

File tree

pages/directory-and-name-resolution.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public Task<SqlDatabase> Build(
113113
string? databaseSuffix = null,
114114
[CallerMemberName] string memberName = "")
115115
```
116-
<sup><a href='/src/LocalDb/SqlInstance.cs#L138-L160' title='Snippet source file'>snippet source</a> | <a href='#snippet-ConventionBuildSignature' title='Start of snippet'>anchor</a></sup>
116+
<sup><a href='/src/LocalDb/SqlInstance.cs#L136-L158' title='Snippet source file'>snippet source</a> | <a href='#snippet-ConventionBuildSignature' title='Start of snippet'>anchor</a></sup>
117117
<!-- endSnippet -->
118118

119119
With these parameters the database name is the derived as follows:
@@ -150,7 +150,7 @@ If full control over the database name is required, there is an overload that ta
150150
/// </summary>
151151
public async Task<SqlDatabase> Build(string dbName)
152152
```
153-
<sup><a href='/src/LocalDb/SqlInstance.cs#L175-L182' title='Snippet source file'>snippet source</a> | <a href='#snippet-ExplicitBuildSignature' title='Start of snippet'>anchor</a></sup>
153+
<sup><a href='/src/LocalDb/SqlInstance.cs#L173-L180' title='Snippet source file'>snippet source</a> | <a href='#snippet-ExplicitBuildSignature' title='Start of snippet'>anchor</a></sup>
154154
<!-- endSnippet -->
155155

156156
Which can be used as follows:

pages/raw-usage.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public async Task SharedDatabase()
271271
AreEqual(0, data.Count);
272272
}
273273
```
274-
<sup><a href='/src/LocalDb.Tests/Tests.cs#L182-L196' title='Snippet source file'>snippet source</a> | <a href='#snippet-SharedDatabase' title='Start of snippet'>anchor</a></sup>
274+
<sup><a href='/src/LocalDb.Tests/Tests.cs#L227-L241' title='Snippet source file'>snippet source</a> | <a href='#snippet-SharedDatabase' title='Start of snippet'>anchor</a></sup>
275275
<!-- endSnippet -->
276276

277277
Pass `useTransaction: true` to get an auto-rolling-back transaction, allowing writes without affecting other tests.
@@ -307,5 +307,5 @@ public async Task SharedDatabase_WithTransaction()
307307
AreEqual(0, data.Count);
308308
}
309309
```
310-
<sup><a href='/src/LocalDb.Tests/Tests.cs#L215-L243' title='Snippet source file'>snippet source</a> | <a href='#snippet-SharedDatabase_WithTransaction' title='Start of snippet'>anchor</a></sup>
310+
<sup><a href='/src/LocalDb.Tests/Tests.cs#L260-L288' title='Snippet source file'>snippet source</a> | <a href='#snippet-SharedDatabase_WithTransaction' title='Start of snippet'>anchor</a></sup>
311311
<!-- endSnippet -->

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project>
33
<PropertyGroup>
44
<NoWarn>CS1591;CA1416;CS8632;NU1608;NU1109</NoWarn>
5-
<Version>24.1.1</Version>
5+
<Version>24.1.2</Version>
66
<LangVersion>preview</LangVersion>
77
<AssemblyVersion>1.0.0</AssemblyVersion>
88
<ContinuousIntegrationBuild>false</ContinuousIntegrationBuild>

src/EfClassicLocalDb/EfClassicLocalDb.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<Compile Include="..\LocalDb\Wrapper.cs" />
3535
<Compile Include="..\LocalDb\State.cs" />
3636
<Compile Include="..\LocalDb\CiDetection.cs" />
37+
<Compile Include="..\LocalDb\AiCliDetector.cs" />
3738

3839
<!-- explicit ref to avoid CVE -->
3940
<PackageReference Include="System.Drawing.Common" />
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Verifies that, when AiCliDetector reports an AI session, every name/directory that flows
2+
// into a SqlInstance carries the chatbot_ prefix — regardless of whether it was derived
3+
// from the DbContext, produced by Storage.FromSuffix, or supplied directly by the caller
4+
// via `new Storage(name, directory)`. The prefixing is centralised in the Storage
5+
// constructor + AiCliDetector helpers, so all entry points pick it up. See commit 2cafc032.
6+
7+
[TestFixture]
8+
public class AiCliDetectorPrefixTests
9+
{
10+
class FakeContext;
11+
12+
[Test]
13+
public void FromSuffix_PrefixesNameAndDirectory_WhenAiDetected()
14+
{
15+
var original = AiCliDetector.Detected;
16+
try
17+
{
18+
AiCliDetector.Detected = true;
19+
20+
var storage = Storage.FromSuffix<FakeContext>("Worker1");
21+
22+
That(storage.Name, Is.EqualTo("chatbot_FakeContext_Worker1"));
23+
That(Path.GetFileName(storage.Directory), Is.EqualTo("chatbot_FakeContext_Worker1"));
24+
}
25+
finally
26+
{
27+
AiCliDetector.Detected = original;
28+
}
29+
}
30+
31+
[Test]
32+
public void FromSuffix_LeavesNameUnprefixed_WhenNotDetected()
33+
{
34+
var original = AiCliDetector.Detected;
35+
try
36+
{
37+
AiCliDetector.Detected = false;
38+
39+
var storage = Storage.FromSuffix<FakeContext>("Worker1");
40+
41+
That(storage.Name, Is.EqualTo("FakeContext_Worker1"));
42+
That(Path.GetFileName(storage.Directory), Is.EqualTo("FakeContext_Worker1"));
43+
}
44+
finally
45+
{
46+
AiCliDetector.Detected = original;
47+
}
48+
}
49+
50+
[Test]
51+
public void CustomStorage_PrefixesNameAndDirectoryLeaf_WhenAiDetected()
52+
{
53+
var original = AiCliDetector.Detected;
54+
try
55+
{
56+
AiCliDetector.Detected = true;
57+
58+
var storage = new Storage("MyCustomInstance", @"C:\TestDatabases\MyApp");
59+
60+
That(storage.Name, Is.EqualTo("chatbot_MyCustomInstance"));
61+
That(storage.Directory, Is.EqualTo(@"C:\TestDatabases\chatbot_MyApp"));
62+
}
63+
finally
64+
{
65+
AiCliDetector.Detected = original;
66+
}
67+
}
68+
69+
[Test]
70+
public void CustomStorage_LeavesUserInputAlone_WhenNotDetected()
71+
{
72+
var original = AiCliDetector.Detected;
73+
try
74+
{
75+
AiCliDetector.Detected = false;
76+
77+
var storage = new Storage("MyCustomInstance", @"C:\TestDatabases\MyApp");
78+
79+
That(storage.Name, Is.EqualTo("MyCustomInstance"));
80+
That(storage.Directory, Is.EqualTo(@"C:\TestDatabases\MyApp"));
81+
}
82+
finally
83+
{
84+
AiCliDetector.Detected = original;
85+
}
86+
}
87+
88+
[Test]
89+
public void PrefixIfDetected_IsIdempotent()
90+
{
91+
var original = AiCliDetector.Detected;
92+
try
93+
{
94+
AiCliDetector.Detected = true;
95+
96+
That(AiCliDetector.PrefixIfDetected("chatbot_Foo"), Is.EqualTo("chatbot_Foo"));
97+
That(AiCliDetector.PrefixIfDetected("Foo"), Is.EqualTo("chatbot_Foo"));
98+
}
99+
finally
100+
{
101+
AiCliDetector.Detected = original;
102+
}
103+
}
104+
105+
[Test]
106+
public void PrefixDirectoryIfDetected_IsIdempotent()
107+
{
108+
var original = AiCliDetector.Detected;
109+
try
110+
{
111+
AiCliDetector.Detected = true;
112+
113+
That(
114+
AiCliDetector.PrefixDirectoryIfDetected(@"C:\Data\chatbot_Foo"),
115+
Is.EqualTo(@"C:\Data\chatbot_Foo"));
116+
That(
117+
AiCliDetector.PrefixDirectoryIfDetected(@"C:\Data\Foo"),
118+
Is.EqualTo(@"C:\Data\chatbot_Foo"));
119+
}
120+
finally
121+
{
122+
AiCliDetector.Detected = original;
123+
}
124+
}
125+
126+
[Test]
127+
public void PrefixIfDetected_ReturnsInputUnchanged_WhenNotDetected()
128+
{
129+
var original = AiCliDetector.Detected;
130+
try
131+
{
132+
AiCliDetector.Detected = false;
133+
That(AiCliDetector.PrefixIfDetected("Foo"), Is.EqualTo("Foo"));
134+
That(AiCliDetector.PrefixDirectoryIfDetected(@"C:\Data\Foo"), Is.EqualTo(@"C:\Data\Foo"));
135+
}
136+
finally
137+
{
138+
AiCliDetector.Detected = original;
139+
}
140+
}
141+
}

src/EfLocalDb/EfLocalDb.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<Compile Include="..\LocalDb\Wrapper.cs" />
3333
<Compile Include="..\LocalDb\State.cs" />
3434
<Compile Include="..\LocalDb\CiDetection.cs" />
35+
<Compile Include="..\LocalDb\AiCliDetector.cs" />
3536

3637
<PackageReference Include="MethodTimer.Fody" PrivateAssets="All" />
3738
<PackageReference Include="ConfigureAwait.Fody" PrivateAssets="All" />

src/EfLocalDb/Storage.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ public Storage(string name, string directory)
6363
{
6464
Ensure.NotNullOrWhiteSpace(directory);
6565
Ensure.NotNullOrWhiteSpace(name);
66-
Name = name;
67-
Directory = directory;
66+
Name = AiCliDetector.PrefixIfDetected(name);
67+
Directory = AiCliDetector.PrefixDirectoryIfDetected(directory);
6868
}
6969

7070
/// <summary>

src/LocalDb.Tests/Tests.cs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,56 @@ public async Task Multiple()
172172
[Test]
173173
public void DirectoryParameter_ShouldBeUsed()
174174
{
175-
var customDirectory = Path.Combine(Path.GetTempPath(), "CustomLocalDbDirectory");
176-
var instance = new SqlInstance("DirectoryTest", TestDbBuilder.CreateTable, directory: customDirectory);
177-
var actualDirectory = instance.Wrapper.Directory;
178-
AreEqual(customDirectory, actualDirectory, "The directory parameter should be used, not overwritten");
179-
instance.Cleanup();
175+
var originalDetected = AiCliDetector.Detected;
176+
try
177+
{
178+
// Without AI detection, an explicit directory parameter is used verbatim.
179+
AiCliDetector.Detected = false;
180+
181+
var customDirectory = Path.Combine(Path.GetTempPath(), "CustomLocalDbDirectory");
182+
var instance = new SqlInstance("DirectoryTest", TestDbBuilder.CreateTable, directory: customDirectory);
183+
try
184+
{
185+
var actualDirectory = instance.Wrapper.Directory;
186+
AreEqual(customDirectory, actualDirectory, "The directory parameter should be used, not overwritten");
187+
}
188+
finally
189+
{
190+
instance.Cleanup();
191+
}
192+
}
193+
finally
194+
{
195+
AiCliDetector.Detected = originalDetected;
196+
}
197+
}
198+
199+
[Test]
200+
public void DirectoryParameter_LeafIsPrefixed_WhenAiDetected()
201+
{
202+
var originalDetected = AiCliDetector.Detected;
203+
try
204+
{
205+
// With AI detection, the leaf segment of an explicit directory is prefixed so AI
206+
// and human runs don't share a template folder even with caller-supplied paths.
207+
AiCliDetector.Detected = true;
208+
209+
var customDirectory = Path.Combine(Path.GetTempPath(), "CustomLocalDbDirectory");
210+
var expectedDirectory = Path.Combine(Path.GetTempPath(), "chatbot_CustomLocalDbDirectory");
211+
var instance = new SqlInstance("DirectoryTestAi", TestDbBuilder.CreateTable, directory: customDirectory);
212+
try
213+
{
214+
AreEqual(expectedDirectory, instance.Wrapper.Directory);
215+
}
216+
finally
217+
{
218+
instance.Cleanup();
219+
}
220+
}
221+
finally
222+
{
223+
AiCliDetector.Detected = originalDetected;
224+
}
180225
}
181226

182227
#region SharedDatabase

src/LocalDb/AiCliDetector.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,45 @@ static AiCliDetector()
6464
}
6565

6666
public static bool Detected { get; set; }
67+
68+
public const string Prefix = "chatbot_";
69+
70+
/// <summary>
71+
/// Prepends <see cref="Prefix"/> to <paramref name="name"/> when an AI CLI session is
72+
/// detected, isolating instance / storage names between AI and human-driven runs.
73+
/// Idempotent — a name that already starts with <see cref="Prefix"/> is returned unchanged.
74+
/// </summary>
75+
public static string PrefixIfDetected(string name)
76+
{
77+
if (!Detected || name.StartsWith(Prefix, StringComparison.Ordinal))
78+
{
79+
return name;
80+
}
81+
82+
return Prefix + name;
83+
}
84+
85+
/// <summary>
86+
/// Prepends <see cref="Prefix"/> to the leaf segment of <paramref name="directory"/> when
87+
/// an AI CLI session is detected, so AI and human runs don't share an on-disk template
88+
/// folder even when callers supply an explicit directory.
89+
/// Idempotent — a leaf that already starts with <see cref="Prefix"/> is returned unchanged.
90+
/// </summary>
91+
public static string PrefixDirectoryIfDetected(string directory)
92+
{
93+
if (!Detected)
94+
{
95+
return directory;
96+
}
97+
98+
var leaf = Path.GetFileName(directory);
99+
if (leaf.StartsWith(Prefix, StringComparison.Ordinal))
100+
{
101+
return directory;
102+
}
103+
104+
var parent = Path.GetDirectoryName(directory);
105+
var prefixedLeaf = Prefix + leaf;
106+
return parent is null ? prefixedLeaf : Path.Combine(parent, prefixedLeaf);
107+
}
67108
}

src/LocalDb/SqlInstance.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,7 @@ public SqlInstance(
7979
}
8080

8181
Ensure.NotNullOrWhiteSpace(name);
82-
if (AiCliDetector.Detected)
83-
{
84-
name = "chatbot_" + name;
85-
}
82+
name = AiCliDetector.PrefixIfDetected(name);
8683

8784
if (directory == null)
8885
{
@@ -91,6 +88,7 @@ public SqlInstance(
9188
else
9289
{
9390
Ensure.NotWhiteSpace(directory);
91+
directory = AiCliDetector.PrefixDirectoryIfDetected(directory);
9492
}
9593

9694
this.dbAutoOffline = CiDetection.ResolveDbAutoOffline(dbAutoOffline);

0 commit comments

Comments
 (0)