Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
dotnet run --project src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj --no-build --configuration Release -- test gates
- name: Upload coverage
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: coverage-report
path: ./coverage/**/coverage.cobertura.xml
Expand Down
2 changes: 1 addition & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Scenarios are selected via request **`Metadata`** key **`parityScenario`** (**`P

- **`streaming_text`** — deterministic deltas → `"Hello world"`
- **`stream_failure`** — throws (turn fails; session may become **`Failed`**)
- **`stream_slow`** — delays (cancellation / timeout scenarios)
- **`stream_slow`** — emits an initial delta, then waits for cancellation (timeout / recovery scenarios)

**`AddDeterministicMockModelProvider`** registers the provider + **`PostConfigure<ProviderCatalogOptions>`** so **`default`** / **`deterministic`** aliases point at the mock.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ private async IAsyncEnumerable<ProviderEvent> StreamEventsAsync(
case ParityProviderScenario.StreamSlow:
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
yield return CreateDelta(request, sequence: 1, "slow-start");
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
yield return CreateTerminal(request, sequence: 2);
// Keep cancellation scenarios active even if a loaded runner observes the turn late.
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false);
yield break;
case ParityProviderScenario.ToolCallRoundtrip:
// Check if the request already contains tool-result content (second iteration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static class ParityProviderScenario
public const string StreamFailure = "stream_failure";

/// <summary>
/// Delays long enough to let timeout and recovery scenarios cancel the stream.
/// Emits an initial delta, then waits for timeout and recovery scenarios to cancel the stream.
/// </summary>
public const string StreamSlow = "stream_slow";

Expand Down
3 changes: 2 additions & 1 deletion tests/SharpClaw.Code.ParityHarness/ParityScenarioTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,8 @@ private async Task RunPromptWithCancelAfterTurnStartAsync(

private async Task WaitForActiveTurnAsync(ISessionStore store, CancellationToken cancellationToken)
{
for (var attempt = 0; attempt < 100; attempt++)
// Startup is separate from the cancellation deadline and can be slow on loaded Windows runners.
for (var attempt = 0; attempt < 600; attempt++)
{
var session = await store.GetLatestAsync(_workspace, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(session?.ActiveTurnId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,24 @@ public async Task StartStreamAsync_should_emit_deterministic_ids_and_timestamps(
new DateTimeOffset(2026, 4, 6, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(2),
new DateTimeOffset(2026, 4, 6, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(3));
}
[Fact]
public async Task Slow_stream_should_remain_active_until_canceled_after_delayed_observation()
{
var provider = new DeterministicMockModelProvider();
var request = new ProviderRequest(
"slow-request", "session", "turn", DeterministicMockModelProvider.ProviderNameConstant,
DeterministicMockModelProvider.DefaultModelId, "slow", null, OutputFormat.Text, null,
new Dictionary<string, string> { [ParityMetadataKeys.Scenario] = ParityProviderScenario.StreamSlow });
using var cancellation = new CancellationTokenSource();
var handle = await provider.StartStreamAsync(request, cancellation.Token);
await using var events = handle.Events.GetAsyncEnumerator(cancellation.Token);
Assert.True(await events.MoveNextAsync());

var nextEvent = events.MoveNextAsync().AsTask();
// Simulate observing the session after the original one-second fixture would have finished.
await Task.Delay(TimeSpan.FromMilliseconds(1500));
Assert.False(nextEvent.IsCompleted);
await cancellation.CancelAsync();
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await nextEvent);
}
}
Loading