@@ -63,6 +63,112 @@ public async Task Quiesce_PreservesProviderDriftAndFailsClosed()
6363 Assert . DoesNotContain ( commands . Calls , call => call . Contains ( "unset" ) ) ;
6464 }
6565
66+ [ Fact ]
67+ public async Task Quiesce_NoProviderWithoutEndpoint_Succeeds ( )
68+ {
69+ LocalAiResolvedInstall install = InstallWithoutEndpoint ( 28_765 ) ;
70+ var commands = new FakeWslCommandRunner ( providerJson : null ) ;
71+ var coordinator = CreateCoordinator ( commands ) ;
72+
73+ LocalAiEndpointLifecycleResult result = await coordinator . QuiesceAsync ( install ) ;
74+
75+ Assert . True ( result . Success ) ;
76+ Assert . Null ( commands . ProviderJson ) ;
77+ Assert . Null ( commands . PrimaryModel ) ;
78+ Assert . DoesNotContain ( commands . Calls , call => call . Contains ( "unset" ) ) ;
79+ }
80+
81+ [ Fact ]
82+ public async Task Quiesce_ExistingProviderWithoutEndpoint_PreservesProviderAndFailsClosed ( )
83+ {
84+ LocalAiResolvedInstall runningInstall = Install ( 28_765 ) ;
85+ string provider = LocalAiGatewayProviderDefinition . BuildProviderJson ( runningInstall ) ;
86+ var commands = new FakeWslCommandRunner (
87+ provider ,
88+ LocalAiGatewayProviderDefinition . BuildPrimaryModel ( runningInstall ) ) ;
89+ var coordinator = CreateCoordinator ( commands ) ;
90+
91+ LocalAiEndpointLifecycleResult result = await coordinator . QuiesceAsync (
92+ InstallWithoutEndpoint ( 28_765 ) ) ;
93+
94+ Assert . False ( result . Success ) ;
95+ Assert . Contains ( "verified Local AI endpoint is required" , result . Detail , StringComparison . Ordinal ) ;
96+ Assert . Equal ( provider , commands . ProviderJson ) ;
97+ Assert . Equal ( LocalAiGatewayProviderDefinition . BuildPrimaryModel ( runningInstall ) , commands . PrimaryModel ) ;
98+ Assert . DoesNotContain ( commands . Calls , call => call . Contains ( "unset" ) ) ;
99+ }
100+
101+ [ Theory ]
102+ [ InlineData ( true ) ]
103+ [ InlineData ( false ) ]
104+ public async Task Quiesce_NoProviderWithUnqualifiedManagedModel_PreservesPrimaryAndFailsClosed (
105+ bool unknownCatalog )
106+ {
107+ LocalAiResolvedInstall valid = InstallWithoutEndpoint ( 28_765 ) ;
108+ LocalAiResolvedInstall tampered = valid with
109+ {
110+ Manifest = valid . Manifest with
111+ {
112+ ModelCatalogId = unknownCatalog ? "missing-model" : valid . Manifest . ModelCatalogId ,
113+ ModelAlias = unknownCatalog ? valid . Manifest . ModelAlias : "tampered-model" ,
114+ } ,
115+ } ;
116+ string primary = $ "llamacpp/{ tampered . Manifest . ModelAlias } ";
117+ var commands = new FakeWslCommandRunner ( providerJson : null , primary ) ;
118+ var coordinator = CreateCoordinator ( commands ) ;
119+
120+ LocalAiEndpointLifecycleResult result = await coordinator . QuiesceAsync ( tampered ) ;
121+
122+ Assert . False ( result . Success ) ;
123+ Assert . Contains ( "qualified" , result . Detail , StringComparison . Ordinal ) ;
124+ Assert . Null ( commands . ProviderJson ) ;
125+ Assert . Equal ( primary , commands . PrimaryModel ) ;
126+ Assert . DoesNotContain ( commands . Calls , call => call . Contains ( "unset" ) ) ;
127+ Assert . DoesNotContain ( commands . Calls , call => call . Contains ( "/bin/sh" ) ) ;
128+ }
129+
130+ [ Fact ]
131+ public async Task Quiesce_NoProviderWithoutEndpoint_UnsetsManagedPrimary ( )
132+ {
133+ LocalAiResolvedInstall install = InstallWithoutEndpoint ( 28_765 ) ;
134+ var commands = new FakeWslCommandRunner (
135+ providerJson : null ,
136+ LocalAiGatewayProviderDefinition . BuildPrimaryModel ( install ) ) ;
137+ var coordinator = CreateCoordinator ( commands ) ;
138+
139+ LocalAiEndpointLifecycleResult result = await coordinator . QuiesceAsync ( install ) ;
140+
141+ Assert . True ( result . Success ) ;
142+ Assert . Null ( commands . ProviderJson ) ;
143+ Assert . Null ( commands . PrimaryModel ) ;
144+ Assert . Contains ( commands . Calls , call =>
145+ call . Contains ( "unset" ) &&
146+ call . Contains ( LocalAiGatewayProviderDefinition . PrimaryModelPath ) ) ;
147+ }
148+
149+ [ Fact ]
150+ public async Task Quiesce_NoProviderWithoutEndpoint_RestoresFallbackPrimary ( )
151+ {
152+ LocalAiResolvedInstall install = InstallWithoutEndpoint ( 28_765 , "openai/gpt-5" ) ;
153+ var commands = new FakeWslCommandRunner (
154+ providerJson : null ,
155+ LocalAiGatewayProviderDefinition . BuildPrimaryModel ( install ) )
156+ {
157+ PrimaryAfterApply = "openai/gpt-5" ,
158+ } ;
159+ var coordinator = CreateCoordinator ( commands ) ;
160+
161+ LocalAiEndpointLifecycleResult result = await coordinator . QuiesceAsync ( install ) ;
162+
163+ Assert . True ( result . Success ) ;
164+ Assert . Null ( commands . ProviderJson ) ;
165+ Assert . Equal ( "openai/gpt-5" , commands . PrimaryModel ) ;
166+ Assert . Contains ( commands . Calls , call => call . Contains ( "/bin/sh" ) ) ;
167+ Assert . DoesNotContain ( commands . Calls , call =>
168+ call . Contains ( "unset" ) &&
169+ call . Contains ( LocalAiGatewayProviderDefinition . ProviderPath ) ) ;
170+ }
171+
66172 [ Fact ]
67173 public async Task Publish_UsesVerifiedEndpointAndNonDefaultManagedDistro ( )
68174 {
@@ -322,6 +428,18 @@ private static LocalAiResolvedInstall Install(int port, string? fallbackModel =
322428 return new ( manifest , "llama-server.exe" , "model.gguf" , endpoint ) ;
323429 }
324430
431+ private static LocalAiResolvedInstall InstallWithoutEndpoint (
432+ int port ,
433+ string ? fallbackModel = null )
434+ {
435+ LocalAiResolvedInstall install = Install ( port , fallbackModel ) ;
436+ return install with
437+ {
438+ Manifest = install . Manifest with { Endpoint = null } ,
439+ Endpoint = null ,
440+ } ;
441+ }
442+
325443 private static string RedactApiKey ( string value ) => value . Replace (
326444 "\" api\" :\" openai-completions\" ,\" apiKey\" :\" llama-local\" " ,
327445 $ "\" apiKey\" :\" { LocalAiGatewayProviderDefinition . CliRedactedApiKey } \" ,\" api\" :\" openai-completions\" ",
0 commit comments