Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7e8129f
feat: Fiesta pairing improvements — NIC fix, pairing string, LAN push…
PureWeen Mar 9, 2026
38a1de7
fix: close two race conditions in Fiesta pairing
PureWeen Mar 9, 2026
a0e9704
fix: address PR #322 review comments
PureWeen Mar 11, 2026
5d6680e
fix: address PR #322 round 2 review — concurrent send race, deny deli…
PureWeen Mar 11, 2026
1339a19
fix: PR #322 round 3 -- SendComplete guard, inline deny, 256KB size l…
PureWeen Mar 11, 2026
a5a28da
fix: prevent Process.HasExited race causing UnobservedTaskException
PureWeen Mar 12, 2026
d855df5
fix: PR 322 review fixes - overlay visibility, TCS race, JsonDocument…
PureWeen Mar 12, 2026
b3fac59
Add onboarding instructions to Direct Connection and Fiesta Workers s…
PureWeen Mar 13, 2026
2073ff6
Fix Razor parse error: escape @ in @worker-name mention example
PureWeen Mar 13, 2026
eaeeb3b
fix: address security review findings - TimeoutException, injection, …
PureWeen Mar 13, 2026
5729f36
fix: address security review round 2 - path traversal, specific catch…
PureWeen Mar 13, 2026
a4b6e76
Security: use base64 to safely embed GitHub auth token in SSH command
PureWeen Mar 13, 2026
e54eb98
fix: use Tailscale IP in Fiesta pairing string when available
PureWeen Mar 13, 2026
a687983
Fix: Fiesta pairing string and discovery use Tailscale IP when available
PureWeen Mar 13, 2026
2786d30
fix: use Tailscale IP in push-to-pair approval response
PureWeen Mar 13, 2026
a0b0702
feat: add Tailscale installation instructions to Settings
PureWeen Mar 13, 2026
a55d1b9
fix: harden structural test and add behavior test for process error f…
PureWeen Mar 15, 2026
8b86804
fix: address PR 322 round 10 review findings
PureWeen Mar 16, 2026
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
60 changes: 60 additions & 0 deletions PolyPilot.Tests/ConnectionRecoveryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,66 @@ public void RestorePreviousSessions_FallbackCoversProcessErrors()
"IsProcessError must be included in the RestorePreviousSessionsAsync fallback condition (not found after the 'Session not found' anchor)");
}

// ===== Behavior test: process error → CreateSessionAsync fallback =====
// Proves that when RestorePreviousSessionsAsync encounters a stale CLI server process,
// the session is recreated via CreateSessionAsync rather than silently dropped.
//
// Architecture note: CopilotClient is a concrete SDK class (not mockable), and
// ResumeSessionAsync is not virtual, so we can't inject a throwing client through
// the full RestorePreviousSessionsAsync pipeline. Instead, this test verifies the
// behavioral contract at the seam: IsProcessError detects the exception, and
// CreateSessionAsync (the fallback) successfully creates the replacement session.
// The structural test above guarantees these are wired together in RestorePreviousSessionsAsync.

[Fact]
public async Task ProcessError_DuringRestore_FallbackCreatesSession()
{
// GIVEN: a process error exception (CLI server died, stale handle)
var processError = new InvalidOperationException("No process is associated with this object.");

// WHEN: IsProcessError evaluates it
Assert.True(CopilotService.IsProcessError(processError));
// Also detected as a connection error (broader category)
Assert.True(CopilotService.IsConnectionError(processError));

// THEN: the CreateSessionAsync fallback path works — session is created and accessible
var svc = CreateService();
await svc.ReconnectAsync(new PolyPilot.Models.ConnectionSettings
{
Mode = PolyPilot.Models.ConnectionMode.Demo
});

var fallbackSession = await svc.CreateSessionAsync("Recovered Session", "gpt-4");
Assert.NotNull(fallbackSession);
Assert.Equal("Recovered Session", fallbackSession.Name);

var allSessions = svc.GetAllSessions().Select(s => s.Name).ToList();
Assert.Contains("Recovered Session", allSessions);
}

[Fact]
public async Task ProcessError_WrappedInAggregate_FallbackCreatesSession()
{
// GIVEN: a process error wrapped in AggregateException (from TaskScheduler.UnobservedTaskException)
var inner = new InvalidOperationException("No process is associated with this object.");
var aggregate = new AggregateException("A Task's exception(s) were not observed", inner);

// WHEN: IsProcessError evaluates the wrapped exception
Assert.True(CopilotService.IsProcessError(aggregate));
Assert.True(CopilotService.IsConnectionError(aggregate));

// THEN: the fallback path works
var svc = CreateService();
await svc.ReconnectAsync(new PolyPilot.Models.ConnectionSettings
{
Mode = PolyPilot.Models.ConnectionMode.Demo
});

var session = await svc.CreateSessionAsync("Recovered Aggregate", "gpt-4");
Assert.NotNull(session);
Assert.Equal("Recovered Aggregate", session.Name);
}

// ===== SafeFireAndForget task observation =====
// Prevents UnobservedTaskException from fire-and-forget _chatDb calls.
// See crash log: "A Task's exception(s) were not observed" wrapping ConnectionLostException.
Expand Down
Loading