Skip to content

Commit 7a10caa

Browse files
committed
Stabilize real Gemini auth test isolation
1 parent 3464dc2 commit 7a10caa

8 files changed

Lines changed: 248 additions & 112 deletions

File tree

GeminiSharpSDK.Tests/Integration/GeminiExecIntegrationTests.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
using ManagedCode.GeminiSharpSDK.Client;
22
using ManagedCode.GeminiSharpSDK.Execution;
33
using ManagedCode.GeminiSharpSDK.Tests.Shared;
4+
using ManagedCode.GeminiSharpSDK.Tests.TestSupport;
45

56
namespace ManagedCode.GeminiSharpSDK.Tests.Integration;
67

78
[Property("RequiresGeminiAuth", "true")]
9+
[ParallelLimiter<GeminiAuthParallelLimit>]
810
public class GeminiExecIntegrationTests
911
{
1012
private const string FirstPrompt = "Reply with short plain text: first.";
1113
private const string SecondPrompt = "Reply with short plain text: second.";
1214
private const string InvalidModel = "__geminisharp_invalid_model__";
15+
private const string SandboxPrefix = "GeminiExecIntegrationTests";
16+
private static readonly TimeSpan SandboxCommandTimeout = TimeSpan.FromSeconds(30);
1317

1418
[Test]
1519
public async Task RunAsync_UsesDefaultProcessRunner_EndToEnd()
1620
{
1721
var settings = RealGeminiTestSupport.GetRequiredSettings();
22+
using var sandbox = await RealGeminiTestSandbox.CreateAsync(SandboxPrefix, SandboxCommandTimeout);
1823

1924
var exec = new GeminiExec();
2025
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
@@ -23,6 +28,7 @@ public async Task RunAsync_UsesDefaultProcessRunner_EndToEnd()
2328
{
2429
Input = FirstPrompt,
2530
Model = settings.Model,
31+
WorkingDirectory = sandbox.WorkingDirectory,
2632
CancellationToken = cancellation.Token,
2733
}));
2834

@@ -34,14 +40,12 @@ public async Task RunAsync_UsesDefaultProcessRunner_EndToEnd()
3440
public async Task RunAsync_SecondCallPassesResumeArgument_EndToEnd()
3541
{
3642
var settings = RealGeminiTestSupport.GetRequiredSettings();
43+
using var sandbox = await RealGeminiTestSandbox.CreateAsync(SandboxPrefix, SandboxCommandTimeout);
3744

3845
using var client = RealGeminiTestSupport.CreateClient();
3946
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
4047

41-
var thread = client.StartThread(new ThreadOptions
42-
{
43-
Model = settings.Model,
44-
});
48+
var thread = client.StartThread(sandbox.CreateThreadOptions(settings.Model, ephemeral: false));
4549

4650
var firstResult = await thread.RunAsync(
4751
FirstPrompt,
@@ -63,6 +67,7 @@ public async Task RunAsync_SecondCallPassesResumeArgument_EndToEnd()
6367
public async Task RunAsync_PropagatesNonZeroExitCode_EndToEnd()
6468
{
6569
var settings = RealGeminiTestSupport.GetRequiredSettings();
70+
using var sandbox = await RealGeminiTestSandbox.CreateAsync(SandboxPrefix, SandboxCommandTimeout);
6671

6772
var exec = new GeminiExec();
6873
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
@@ -71,6 +76,7 @@ public async Task RunAsync_PropagatesNonZeroExitCode_EndToEnd()
7176
{
7277
Input = FirstPrompt,
7378
Model = InvalidModel,
79+
WorkingDirectory = sandbox.WorkingDirectory,
7480
CancellationToken = cancellation.Token,
7581
}));
7682

GeminiSharpSDK.Tests/Integration/RealGeminiIntegrationTests.cs

Lines changed: 41 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,16 @@
44
using ManagedCode.GeminiSharpSDK.Internal;
55
using ManagedCode.GeminiSharpSDK.Models;
66
using ManagedCode.GeminiSharpSDK.Tests.Shared;
7+
using ManagedCode.GeminiSharpSDK.Tests.TestSupport;
78

89
namespace ManagedCode.GeminiSharpSDK.Tests.Integration;
910

1011
[Property("RequiresGeminiAuth", "true")]
12+
[ParallelLimiter<GeminiAuthParallelLimit>]
1113
public class RealGeminiIntegrationTests
1214
{
13-
private const string SolutionFileName = "ManagedCode.GeminiSharpSDK.slnx";
14-
private const string TestsDirectoryName = "tests";
15-
private const string SandboxDirectoryName = ".sandbox";
16-
private const string SandboxPrefix = "RealGeminiIntegrationTests-SessionVisibility-";
17-
private const string GitExecutableName = "git";
18-
private const string GitInitArgument = "init";
19-
private const string QuietArgument = "-q";
15+
private const string SandboxPrefix = "RealGeminiIntegrationTests";
16+
private const string SessionVisibilitySandboxPrefix = "RealGeminiIntegrationTests-SessionVisibility";
2017
private const string ListSessionsFlag = "--list-sessions";
2118
private const string GeminiDirectoryName = ".gemini";
2219
private const string ProjectsFileName = "projects.json";
@@ -34,9 +31,10 @@ public class RealGeminiIntegrationTests
3431
public async Task RunAsync_WithRealGeminiCli_ReturnsStructuredOutput()
3532
{
3633
var settings = RealGeminiTestSupport.GetRequiredSettings();
34+
using var sandbox = await RealGeminiTestSandbox.CreateAsync(SandboxPrefix, CliCommandTimeout);
3735

3836
using var client = RealGeminiTestSupport.CreateClient();
39-
var thread = StartRealIntegrationThread(client, settings.Model);
37+
var thread = StartRealIntegrationThread(client, settings.Model, sandbox.WorkingDirectory);
4038

4139
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
4240
var schema = IntegrationOutputSchemas.StatusOnly();
@@ -55,9 +53,10 @@ public async Task RunAsync_WithRealGeminiCli_ReturnsStructuredOutput()
5553
public async Task RunStreamedAsync_WithRealGeminiCli_YieldsCurrentStreamJsonEvents()
5654
{
5755
var settings = RealGeminiTestSupport.GetRequiredSettings();
56+
using var sandbox = await RealGeminiTestSandbox.CreateAsync(SandboxPrefix, CliCommandTimeout);
5857

5958
using var client = RealGeminiTestSupport.CreateClient();
60-
var thread = StartRealIntegrationThread(client, settings.Model);
59+
var thread = StartRealIntegrationThread(client, settings.Model, sandbox.WorkingDirectory);
6160
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
6261

6362
var streamed = await thread.RunStreamedAsync(
@@ -85,9 +84,10 @@ public async Task RunStreamedAsync_WithRealGeminiCli_YieldsCurrentStreamJsonEven
8584
public async Task RunAsync_WithRealGeminiCli_SecondTurnKeepsThreadId()
8685
{
8786
var settings = RealGeminiTestSupport.GetRequiredSettings();
87+
using var sandbox = await RealGeminiTestSandbox.CreateAsync(SandboxPrefix, CliCommandTimeout);
8888

8989
using var client = RealGeminiTestSupport.CreateClient();
90-
var thread = StartRealIntegrationThread(client, settings.Model);
90+
var thread = StartRealIntegrationThread(client, settings.Model, sandbox.WorkingDirectory);
9191
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
9292

9393
var schema = IntegrationOutputSchemas.StatusOnly();
@@ -117,99 +117,47 @@ public async Task RunAsync_WithRealGeminiCli_SecondTurnKeepsThreadId()
117117
public async Task RunAsync_WithFreshWorkingDirectory_PersistsSessionVisibleToGeminiCli()
118118
{
119119
var settings = RealGeminiTestSupport.GetRequiredSettings();
120-
var sandboxDirectory = await CreateGitSandboxDirectoryAsync();
120+
using var sandbox = await RealGeminiTestSandbox.CreateAsync(
121+
SessionVisibilitySandboxPrefix,
122+
CliCommandTimeout);
123+
var sandboxDirectory = sandbox.WorkingDirectory;
121124

122-
try
123-
{
124-
using var client = RealGeminiTestSupport.CreateClient();
125-
var thread = client.StartThread(new ThreadOptions
126-
{
127-
Model = settings.Model,
128-
WorkingDirectory = sandboxDirectory,
129-
Ephemeral = false,
130-
});
131-
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
132-
133-
var result = await thread.RunAsync(
134-
ProjectSessionVisiblePrompt,
135-
new TurnOptions { CancellationToken = cancellation.Token });
136-
137-
await Assert.That(result.Usage).IsNotNull();
138-
await Assert.That(thread.Id).IsNotNull();
139-
140-
var persistedSessionPath = await FindPersistedSessionPathAsync(
141-
sandboxDirectory,
142-
thread.Id!,
143-
SessionVisibilityTimeout);
144-
145-
await Assert.That(persistedSessionPath).IsNotNull();
146-
147-
var listSessionsResult = await RunGeminiAsync(
148-
sandboxDirectory,
149-
CliCommandTimeout,
150-
ListSessionsFlag);
151-
152-
await Assert.That(listSessionsResult.ExitCode).IsEqualTo(0);
153-
await Assert.That(string.Concat(listSessionsResult.StandardOutput, listSessionsResult.StandardError))
154-
.Contains(thread.Id!);
155-
}
156-
finally
157-
{
158-
if (Directory.Exists(sandboxDirectory))
159-
{
160-
Directory.Delete(sandboxDirectory, recursive: true);
161-
}
162-
}
163-
}
125+
using var client = RealGeminiTestSupport.CreateClient();
126+
var thread = client.StartThread(sandbox.CreateThreadOptions(settings.Model, ephemeral: false));
127+
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
164128

165-
private static GeminiThread StartRealIntegrationThread(GeminiClient client, string model)
166-
{
167-
return client.StartThread(new ThreadOptions
168-
{
169-
Model = model,
170-
});
171-
}
129+
var result = await thread.RunAsync(
130+
ProjectSessionVisiblePrompt,
131+
new TurnOptions { CancellationToken = cancellation.Token });
172132

173-
private static async Task<string> CreateGitSandboxDirectoryAsync()
174-
{
175-
var repositoryRoot = ResolveRepositoryRootPath();
176-
var sandboxDirectory = Path.Combine(
177-
repositoryRoot,
178-
TestsDirectoryName,
179-
SandboxDirectoryName,
180-
$"{SandboxPrefix}{Guid.NewGuid():N}");
181-
182-
Directory.CreateDirectory(sandboxDirectory);
183-
var gitInitResult = await RunCommand(
184-
GitExecutableName,
133+
await Assert.That(result.Usage).IsNotNull();
134+
await Assert.That(thread.Id).IsNotNull();
135+
136+
var persistedSessionPath = await FindPersistedSessionPathAsync(
185137
sandboxDirectory,
186-
CliCommandTimeout,
187-
GitInitArgument,
188-
QuietArgument);
138+
thread.Id!,
139+
SessionVisibilityTimeout);
189140

190-
if (gitInitResult.ExitCode != 0)
191-
{
192-
throw new InvalidOperationException(
193-
$"Failed to initialize git sandbox: {gitInitResult.StandardError}");
194-
}
141+
await Assert.That(persistedSessionPath).IsNotNull();
142+
143+
var listSessionsResult = await RunGeminiAsync(
144+
sandboxDirectory,
145+
CliCommandTimeout,
146+
ListSessionsFlag);
195147

196-
return sandboxDirectory;
148+
await Assert.That(listSessionsResult.ExitCode).IsEqualTo(0);
149+
await Assert.That(string.Concat(listSessionsResult.StandardOutput, listSessionsResult.StandardError))
150+
.Contains(thread.Id!);
197151
}
198152

199-
private static string ResolveRepositoryRootPath()
153+
private static GeminiThread StartRealIntegrationThread(GeminiClient client, string model, string workingDirectory)
200154
{
201-
var current = new DirectoryInfo(AppContext.BaseDirectory);
202-
while (current is not null)
155+
return client.StartThread(new ThreadOptions
203156
{
204-
if (File.Exists(Path.Combine(current.FullName, SolutionFileName)))
205-
{
206-
return current.FullName;
207-
}
208-
209-
current = current.Parent;
210-
}
211-
212-
throw new InvalidOperationException("Could not locate repository root from test execution directory.");
157+
Model = model,
158+
WorkingDirectory = workingDirectory,
159+
Ephemeral = false,
160+
});
213161
}
214162

215163
private static async Task<string?> FindPersistedSessionPathAsync(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using TUnit.Core.Interfaces;
2+
3+
namespace ManagedCode.GeminiSharpSDK.Tests.TestSupport;
4+
5+
internal sealed class GeminiAuthParallelLimit : IParallelLimit
6+
{
7+
public int Limit => 1;
8+
}

0 commit comments

Comments
 (0)