11using DotPilot . Core . ChatSessions ;
2+ using DotPilot . Core . AgentBuilder ;
23using DotPilot . Tests . Providers ;
34using Microsoft . Extensions . DependencyInjection ;
45
56namespace DotPilot . Tests . AgentBuilder ;
67
8+ [ NonParallelizable ]
79public sealed class AgentBuilderModelTests
810{
911 [ Test ]
@@ -37,7 +39,9 @@ public async Task GenerateDraftAndSaveAgentUsesEnabledProviderModelWhenModelOver
3739 agent . Name == "Repository Reviewer Agent" &&
3840 agent . ProviderKind == AgentProviderKind . Codex &&
3941 agent . ModelName == "gpt-5.4" ) ;
40- ( await model . OperationMessage ) . Should ( ) . Be ( "Saved Repository Reviewer Agent using Codex." ) ;
42+ workspace . Sessions . Should ( ) . Contain ( session => session . Title == "Session with Repository Reviewer Agent" ) ;
43+ workspace . SelectedSessionId . Should ( ) . NotBeNull ( ) ;
44+ fixture . RequestedRoutes . Should ( ) . Contain ( ShellRoute . Chat ) ;
4145 ( await model . Builder ) ! . StatusMessage . Should ( ) . Contain ( "ready for local desktop execution" ) ;
4246 }
4347
@@ -47,8 +51,8 @@ public async Task BuilderProjectionReflectsSelectedProviderSuggestionAndVersion(
4751 await using var fixture = await CreateFixtureAsync ( ) ;
4852 var model = ActivatorUtilities . CreateInstance < AgentBuilderModel > ( fixture . Provider ) ;
4953
50- await model . SelectedProvider . UpdateAsync (
51- _ => new AgentProviderOption (
54+ await model . HandleSelectedProviderChanged (
55+ new AgentProviderOption (
5256 AgentProviderKind . GitHubCopilot ,
5357 "GitHub Copilot" ,
5458 "copilot" ,
@@ -123,6 +127,18 @@ public async Task HandleSelectedProviderChangedUpdatesModelSuggestionToTheChosen
123127
124128 await model . BuildManually ( CancellationToken . None ) ;
125129 ( await model . ModelName ) . Should ( ) . Be ( "gpt-5" ) ;
130+ await model . SelectedProvider . UpdateAsync (
131+ _ => new AgentProviderOption (
132+ AgentProviderKind . Codex ,
133+ "Codex" ,
134+ "codex" ,
135+ "Codex CLI is ready for local desktop execution." ,
136+ "gpt-5" ,
137+ [ "gpt-5" , "gpt-5.4" ] ,
138+ "1.0.0" ,
139+ true ) ,
140+ CancellationToken . None ) ;
141+ await model . SelectedProviderKind . SetAsync ( AgentProviderKind . Codex , CancellationToken . None ) ;
126142
127143 await model . HandleSelectedProviderChanged (
128144 new AgentProviderOption (
@@ -158,6 +174,7 @@ await model.SelectedProvider.UpdateAsync(
158174 "1.0.0" ,
159175 true ) ,
160176 CancellationToken . None ) ;
177+ await model . SelectedProviderKind . SetAsync ( AgentProviderKind . Codex , CancellationToken . None ) ;
161178
162179 await model . HandleSelectedProviderChanged (
163180 new AgentProviderOption (
@@ -175,6 +192,89 @@ await model.HandleSelectedProviderChanged(
175192 ( await model . SelectedProviderKind ) . Should ( ) . Be ( AgentProviderKind . GitHubCopilot ) ;
176193 }
177194
195+ [ Test ]
196+ public async Task BuilderProjectionUsesSelectedProviderKindWhenThePreviousProviderStateIsStale ( )
197+ {
198+ using var commandScope = CodexCliTestScope . Create ( nameof ( AgentBuilderModelTests ) ) ;
199+ commandScope . WriteVersionCommand ( "codex" , "codex version 1.0.0" ) ;
200+ commandScope . WriteCodexMetadata ( "gpt-5.4" , "gpt-5.4" , "gpt-5" ) ;
201+ commandScope . WriteVersionCommand ( "claude" , "2.0.75 (Claude Code)" ) ;
202+ commandScope . WriteClaudeSettings ( "claude-opus-4-6" ) ;
203+
204+ await using var fixture = await CreateFixtureAsync ( ) ;
205+ ( await fixture . WorkspaceState . UpdateProviderAsync (
206+ new UpdateProviderPreferenceCommand ( AgentProviderKind . Codex , true ) ,
207+ CancellationToken . None ) ) . ShouldSucceed ( ) ;
208+ ( await fixture . WorkspaceState . UpdateProviderAsync (
209+ new UpdateProviderPreferenceCommand ( AgentProviderKind . ClaudeCode , true ) ,
210+ CancellationToken . None ) ) . ShouldSucceed ( ) ;
211+
212+ var model = ActivatorUtilities . CreateInstance < AgentBuilderModel > ( fixture . Provider ) ;
213+
214+ await model . BuildManually ( CancellationToken . None ) ;
215+ await model . SelectedProvider . UpdateAsync (
216+ _ => new AgentProviderOption (
217+ AgentProviderKind . Debug ,
218+ string . Empty ,
219+ string . Empty ,
220+ string . Empty ,
221+ string . Empty ,
222+ [ ] ,
223+ null ,
224+ false ) ,
225+ CancellationToken . None ) ;
226+ await model . SelectedProviderKind . SetAsync ( AgentProviderKind . ClaudeCode , CancellationToken . None ) ;
227+
228+ var builder = await model . Builder ;
229+
230+ builder . Should ( ) . NotBeNull ( ) ;
231+
232+ builder ! . ProviderDisplayName . Should ( ) . Be ( "Claude Code" ) ;
233+ builder . SuggestedModelName . Should ( ) . Be ( "claude-opus-4-6" ) ;
234+ ( await model . SelectedProvider ) . Should ( ) . NotBeNull ( ) ;
235+ ( await model . SelectedProvider ) ! . Kind . Should ( ) . Be ( AgentProviderKind . ClaudeCode ) ;
236+ }
237+
238+ [ Test ]
239+ public async Task BuilderProjectionPrefersTheSelectedProviderWhenTheProviderKindStateIsStale ( )
240+ {
241+ using var commandScope = CodexCliTestScope . Create ( nameof ( AgentBuilderModelTests ) ) ;
242+ commandScope . WriteVersionCommand ( "codex" , "codex version 1.0.0" ) ;
243+ commandScope . WriteCodexMetadata ( "gpt-5.4" , "gpt-5.4" , "gpt-5" ) ;
244+ commandScope . WriteVersionCommand ( "claude" , "2.0.75 (Claude Code)" ) ;
245+ commandScope . WriteClaudeSettings ( "claude-opus-4-6" ) ;
246+
247+ await using var fixture = await CreateFixtureAsync ( ) ;
248+ ( await fixture . WorkspaceState . UpdateProviderAsync (
249+ new UpdateProviderPreferenceCommand ( AgentProviderKind . Codex , true ) ,
250+ CancellationToken . None ) ) . ShouldSucceed ( ) ;
251+ ( await fixture . WorkspaceState . UpdateProviderAsync (
252+ new UpdateProviderPreferenceCommand ( AgentProviderKind . ClaudeCode , true ) ,
253+ CancellationToken . None ) ) . ShouldSucceed ( ) ;
254+
255+ var model = ActivatorUtilities . CreateInstance < AgentBuilderModel > ( fixture . Provider ) ;
256+
257+ await model . BuildManually ( CancellationToken . None ) ;
258+ await model . SelectedProvider . UpdateAsync (
259+ _ => new AgentProviderOption (
260+ AgentProviderKind . ClaudeCode ,
261+ "Claude Code" ,
262+ "claude" ,
263+ "Claude Code profile authoring is available." ,
264+ "claude-opus-4-6" ,
265+ [ "claude-opus-4-6" , "claude-sonnet-4-5" ] ,
266+ "2.0.75" ,
267+ true ) ,
268+ CancellationToken . None ) ;
269+ await model . SelectedProviderKind . SetAsync ( AgentProviderKind . Codex , CancellationToken . None ) ;
270+
271+ var builder = await model . Builder ;
272+
273+ builder . Should ( ) . NotBeNull ( ) ;
274+ builder ! . ProviderDisplayName . Should ( ) . Be ( "Claude Code" ) ;
275+ builder . SuggestedModelName . Should ( ) . Be ( "claude-opus-4-6" ) ;
276+ }
277+
178278 [ Test ]
179279 public async Task BuildManuallyAllowsChoosingASupportedModelBeforeSavingAgent ( )
180280 {
@@ -221,17 +321,30 @@ public async Task StartChatForAgentCreatesAndSelectsSessionForChosenCatalogAgent
221321 await model . GenerateAgentDraft ( CancellationToken . None ) ;
222322 await model . SaveAgent ( CancellationToken . None ) ;
223323
224- var createdAgent = ( await model . Agents )
324+ var workspace = ( await fixture . WorkspaceState . GetWorkspaceAsync ( CancellationToken . None ) ) . ShouldSucceed ( ) ;
325+ var createdAgentSummary = workspace . Agents
225326 . Should ( )
226327 . ContainSingle ( agent => agent . Name == "Repository Reviewer Agent" )
227328 . Which ;
329+ var createdAgent = new AgentCatalogItem (
330+ createdAgentSummary . Id ,
331+ "R" ,
332+ createdAgentSummary . Name ,
333+ AgentSessionDefaults . CreateAgentDescription ( createdAgentSummary . SystemPrompt ) ,
334+ createdAgentSummary . ProviderDisplayName ,
335+ createdAgentSummary . ModelName ,
336+ false ,
337+ "AgentCatalogStartChatButton_RepositoryReviewerAgent" ,
338+ new AgentCatalogStartChatRequest ( createdAgentSummary . Id , createdAgentSummary . Name ) ,
339+ null ) ;
228340
229341 await model . StartChatForAgent ( createdAgent , CancellationToken . None ) ;
230342
231- var workspace = ( await fixture . WorkspaceState . GetWorkspaceAsync ( CancellationToken . None ) ) . ShouldSucceed ( ) ;
343+ workspace = ( await fixture . WorkspaceState . GetWorkspaceAsync ( CancellationToken . None ) ) . ShouldSucceed ( ) ;
232344 workspace . Sessions . Should ( ) . Contain ( session => session . Title == "Session with Repository Reviewer Agent" ) ;
233345 workspace . SelectedSessionId . Should ( ) . NotBeNull ( ) ;
234- ( await model . OperationMessage ) . Should ( ) . Be ( "Started a session with Repository Reviewer Agent. Switch to Chat to continue." ) ;
346+ fixture . RequestedRoutes . Should ( ) . Contain ( ShellRoute . Chat ) ;
347+ ( await model . OperationMessage ) . Should ( ) . Be ( "Started a session with Repository Reviewer Agent." ) ;
235348 }
236349
237350 private static async Task < TestFixture > CreateFixtureAsync ( )
@@ -243,21 +356,43 @@ private static async Task<TestFixture> CreateFixtureAsync()
243356 UseInMemoryDatabase = true ,
244357 InMemoryDatabaseName = Guid . NewGuid ( ) . ToString ( "N" ) ,
245358 } ) ;
359+ services . AddSingleton < WorkspaceProjectionNotifier > ( ) ;
360+ services . AddSingleton < ShellNavigationNotifier > ( ) ;
246361
247362 var provider = services . BuildServiceProvider ( ) ;
248363 var workspaceState = provider . GetRequiredService < IAgentWorkspaceState > ( ) ;
249- return new TestFixture ( provider , workspaceState ) ;
364+ var navigationNotifier = provider . GetRequiredService < ShellNavigationNotifier > ( ) ;
365+ return new TestFixture ( provider , workspaceState , navigationNotifier ) ;
250366 }
251367
252- private sealed class TestFixture ( ServiceProvider provider , IAgentWorkspaceState workspaceState ) : IAsyncDisposable
368+ private sealed class TestFixture : IAsyncDisposable
253369 {
254- public ServiceProvider Provider { get ; } = provider ;
370+ private readonly ShellNavigationNotifier navigationNotifier ;
371+ private readonly List < ShellRoute > requestedRoutes = [ ] ;
372+
373+ public TestFixture ( ServiceProvider provider , IAgentWorkspaceState workspaceState , ShellNavigationNotifier navigationNotifier )
374+ {
375+ Provider = provider ;
376+ WorkspaceState = workspaceState ;
377+ this . navigationNotifier = navigationNotifier ;
378+ this . navigationNotifier . Requested += OnNavigationRequested ;
379+ }
380+
381+ public ServiceProvider Provider { get ; }
255382
256- public IAgentWorkspaceState WorkspaceState { get ; } = workspaceState ;
383+ public IAgentWorkspaceState WorkspaceState { get ; }
384+
385+ public IReadOnlyList < ShellRoute > RequestedRoutes => requestedRoutes ;
257386
258387 public ValueTask DisposeAsync ( )
259388 {
389+ navigationNotifier . Requested -= OnNavigationRequested ;
260390 return Provider . DisposeAsync ( ) ;
261391 }
392+
393+ private void OnNavigationRequested ( object ? sender , ShellNavigationRequestedEventArgs e )
394+ {
395+ requestedRoutes . Add ( e . Route ) ;
396+ }
262397 }
263398}
0 commit comments