Skip to content

Commit 09df9f8

Browse files
committed
Stabilize repeated real Gemini test runs
1 parent fcc2749 commit 09df9f8

6 files changed

Lines changed: 22 additions & 12 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ _TeamCity*
155155
# Visual Studio code coverage results
156156
*.coverage
157157
*.coveragexml
158+
/tests/.sandbox/
158159

159160
# NCrunch
160161
_NCrunch_*

GeminiSharpSDK.Tests/Integration/GeminiExecIntegrationTests.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class GeminiExecIntegrationTests
1414
private const string InvalidModel = "__geminisharp_invalid_model__";
1515
private const string SandboxPrefix = "GeminiExecIntegrationTests";
1616
private static readonly TimeSpan SandboxCommandTimeout = TimeSpan.FromSeconds(30);
17+
private static readonly TimeSpan MultiTurnTimeout = TimeSpan.FromMinutes(3);
1718

1819
[Test]
1920
public async Task RunAsync_UsesDefaultProcessRunner_EndToEnd()
@@ -43,21 +44,22 @@ public async Task RunAsync_SecondCallPassesResumeArgument_EndToEnd()
4344
using var sandbox = await RealGeminiTestSandbox.CreateAsync(SandboxPrefix, SandboxCommandTimeout);
4445

4546
using var client = RealGeminiTestSupport.CreateClient();
46-
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
4747

4848
var thread = client.StartThread(sandbox.CreateThreadOptions(settings.Model, ephemeral: false));
49+
using var firstCancellation = new CancellationTokenSource(MultiTurnTimeout);
4950

5051
var firstResult = await thread.RunAsync(
5152
FirstPrompt,
52-
new TurnOptions { CancellationToken = cancellation.Token });
53+
new TurnOptions { CancellationToken = firstCancellation.Token });
5354

5455
var threadId = thread.Id;
5556
await Assert.That(threadId).IsNotNull();
5657
await Assert.That(firstResult.Usage).IsNotNull();
5758

59+
using var secondCancellation = new CancellationTokenSource(MultiTurnTimeout);
5860
var secondResult = await thread.RunAsync(
5961
SecondPrompt,
60-
new TurnOptions { CancellationToken = cancellation.Token });
62+
new TurnOptions { CancellationToken = secondCancellation.Token });
6163

6264
await Assert.That(secondResult.Usage).IsNotNull();
6365
await Assert.That(thread.Id).IsEqualTo(threadId);

GeminiSharpSDK.Tests/Integration/RealGeminiIntegrationTests.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class RealGeminiIntegrationTests
2626
private static readonly TimeSpan SessionVisibilityTimeout = TimeSpan.FromSeconds(10);
2727
private static readonly TimeSpan CliCommandTimeout = TimeSpan.FromSeconds(30);
2828
private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(200);
29+
private static readonly TimeSpan MultiTurnTimeout = TimeSpan.FromMinutes(3);
2930

3031
[Test]
3132
public async Task RunAsync_WithRealGeminiCli_ReturnsStructuredOutput()
@@ -88,25 +89,26 @@ public async Task RunAsync_WithRealGeminiCli_SecondTurnKeepsThreadId()
8889

8990
using var client = RealGeminiTestSupport.CreateClient();
9091
var thread = StartRealIntegrationThread(client, settings.Model, sandbox.WorkingDirectory);
91-
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
9292

9393
var schema = IntegrationOutputSchemas.StatusOnly();
94+
using var firstCancellation = new CancellationTokenSource(MultiTurnTimeout);
9495

9596
var first = await thread.RunAsync<StatusResponse>(
9697
"Reply with a JSON object where status is exactly \"ok\".",
9798
schema,
9899
IntegrationOutputJsonContext.Default.StatusResponse,
99-
cancellation.Token);
100+
firstCancellation.Token);
100101

101102
var firstThreadId = thread.Id;
102103
await Assert.That(firstThreadId).IsNotNull();
103104
await Assert.That(first.Usage).IsNotNull();
104105

106+
using var secondCancellation = new CancellationTokenSource(MultiTurnTimeout);
105107
var second = await thread.RunAsync<StatusResponse>(
106108
"Again: reply with a JSON object where status is exactly \"ok\".",
107109
schema,
108110
IntegrationOutputJsonContext.Default.StatusResponse,
109-
cancellation.Token);
111+
secondCancellation.Token);
110112

111113
await Assert.That(second.TypedResponse.Status).IsEqualTo("ok");
112114
await Assert.That(second.Usage).IsNotNull();

GeminiSharpSDK.Tests/Unit/GeminiClientTests.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class GeminiClientTests
1010
{
1111
private const string ResumeSandboxPrefix = "GeminiClientTests-ResumeThread-";
1212
private static readonly TimeSpan SandboxCommandTimeout = TimeSpan.FromSeconds(30);
13+
private static readonly TimeSpan MultiTurnTimeout = TimeSpan.FromMinutes(3);
1314

1415
[Test]
1516
public async Task StartAsync_CanBeCalledConcurrently()
@@ -265,13 +266,13 @@ public async Task ResumeThread_WithThreadOptions_RunsWithRealGeminiCli()
265266
SandboxCommandTimeout);
266267

267268
using var client = RealGeminiTestSupport.CreateClient();
268-
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
269269

270270
var startedThread = client.StartThread(sandbox.CreateThreadOptions(settings.Model, ephemeral: false));
271+
using var firstCancellation = new CancellationTokenSource(MultiTurnTimeout);
271272

272273
var firstResult = await startedThread.RunAsync(
273274
"Reply with short plain text: ok.",
274-
new TurnOptions { CancellationToken = cancellation.Token });
275+
new TurnOptions { CancellationToken = firstCancellation.Token });
275276

276277
var threadId = startedThread.Id;
277278
await Assert.That(threadId).IsNotNull();
@@ -280,10 +281,11 @@ public async Task ResumeThread_WithThreadOptions_RunsWithRealGeminiCli()
280281
var resumedThread = client.ResumeThread(
281282
threadId!,
282283
sandbox.CreateThreadOptions(settings.Model, ephemeral: false));
284+
using var secondCancellation = new CancellationTokenSource(MultiTurnTimeout);
283285

284286
var secondResult = await resumedThread.RunAsync(
285287
"Reply with short plain text: ok.",
286-
new TurnOptions { CancellationToken = cancellation.Token });
288+
new TurnOptions { CancellationToken = secondCancellation.Token });
287289

288290
await Assert.That(secondResult.Usage).IsNotNull();
289291
await Assert.That(resumedThread.Id).IsEqualTo(threadId);

GeminiSharpSDK.Tests/Unit/GeminiThreadTests.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class GeminiThreadTests
1414
{
1515
private const string SandboxPrefix = "GeminiThreadTests";
1616
private static readonly TimeSpan SandboxCommandTimeout = TimeSpan.FromSeconds(30);
17+
private static readonly TimeSpan MultiTurnTimeout = TimeSpan.FromMinutes(3);
1718

1819
[Test]
1920
[Property("RequiresGeminiAuth", "true")]
@@ -124,19 +125,20 @@ public async Task RunAsync_SecondTurnKeepsThreadId_WithRealGeminiCli()
124125

125126
using var client = RealGeminiTestSupport.CreateClient();
126127
var thread = StartRealIntegrationThread(client, settings.Model, sandbox.WorkingDirectory);
127-
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
128+
using var firstCancellation = new CancellationTokenSource(MultiTurnTimeout);
128129

129130
var first = await thread.RunAsync(
130131
"Reply with short plain text: first.",
131-
new TurnOptions { CancellationToken = cancellation.Token });
132+
new TurnOptions { CancellationToken = firstCancellation.Token });
132133

133134
var firstThreadId = thread.Id;
134135
await Assert.That(firstThreadId).IsNotNull();
135136
await Assert.That(first.Usage).IsNotNull();
136137

138+
using var secondCancellation = new CancellationTokenSource(MultiTurnTimeout);
137139
var second = await thread.RunAsync(
138140
"Reply with short plain text: second.",
139-
new TurnOptions { CancellationToken = cancellation.Token });
141+
new TurnOptions { CancellationToken = secondCancellation.Token });
140142

141143
await Assert.That(second.Usage).IsNotNull();
142144
await Assert.That(thread.Id).IsEqualTo(firstThreadId);

docs/Testing/strategy.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Verify `ManagedCode.GeminiSharpSDK` behavior against real Gemini CLI contracts,
2020
- Real integration runs must use existing Gemini CLI login/session; test harness does not use API key environment variables.
2121
- Auth-required real Gemini tests run under a shared parallel limiter so local full-suite execution does not deadlock or stall on concurrent headless CLI sessions.
2222
- Auth-required real Gemini thread/session tests must also run in unique git sandboxes under `tests/.sandbox/*` and keep those directories on disk for the duration of local test history, because Gemini CLI caches visited project paths in `~/.gemini/projects.json` and can fail later startup if a referenced sandbox disappears mid-suite.
23+
- Multi-turn real Gemini tests must allocate cancellation budgets per turn instead of sharing one timeout across the whole conversation, so a slow first turn does not starve the second turn and create flaky resume/thread assertions.
2324
- Real integration model selection must be explicit: set `GEMINI_TEST_MODEL`, define `model` in `~/.gemini/config.toml`, or rely on a recent local Gemini session that already recorded the active model in the current profile.
2425
- Cover error paths and cancellation paths.
2526
- Keep protocol parser coverage for all supported event/item kinds.

0 commit comments

Comments
 (0)