22// Licensed under the MIT License.
33
44using System ;
5+ using System . Collections . Generic ;
56using Microsoft . VisualStudio . TestTools . UnitTesting ;
67
78namespace Regorus . Tests ;
@@ -134,7 +135,7 @@ public void RegisteredHostAwait_Suspendable_SuspendAndResume()
134135 {
135136 var modules = new [ ] { new PolicyModule ( "account.rego" , GetAccountPolicy ) } ;
136137 var entryPoints = new [ ] { "data.demo.allow" } ;
137- var hostAwaitBuiltins = new [ ] { new HostAwaitBuiltin ( "get_account" , 1 ) } ;
138+ var hostAwaitBuiltins = new [ ] { new HostAwaitBuiltin ( "get_account" ) } ;
138139
139140 using var program = Program . CompileFromModules ( "{}" , modules , entryPoints , hostAwaitBuiltins ) ;
140141 using var vm = new Rvm ( ) ;
@@ -145,7 +146,9 @@ public void RegisteredHostAwait_Suspendable_SuspendAndResume()
145146 // Execute — should suspend on get_account()
146147 vm . Execute ( ) ;
147148
148- // Verify we're suspended due to HostAwait with identifier "get_account"
149+ // Verify we're suspended due to HostAwait with identifier "get_account".
150+ // GetHostAwaitIdentifier returns the JSON-encoded Value, so the identifier
151+ // string itself includes the surrounding JSON quotes.
149152 var identifier = vm . GetHostAwaitIdentifier ( ) ;
150153 Assert . AreEqual ( "\" get_account\" " , identifier , "expected identifier to be get_account" ) ;
151154
@@ -174,7 +177,7 @@ public void RegisteredHostAwait_RunToCompletion_WithPreloadedResponses()
174177 {
175178 var modules = new [ ] { new PolicyModule ( "translate.rego" , TranslatePolicy ) } ;
176179 var entryPoints = new [ ] { "data.demo.greeting" } ;
177- var hostAwaitBuiltins = new [ ] { new HostAwaitBuiltin ( "translate" , 1 ) } ;
180+ var hostAwaitBuiltins = new [ ] { new HostAwaitBuiltin ( "translate" ) } ;
178181
179182 using var program = Program . CompileFromModules ( "{}" , modules , entryPoints , hostAwaitBuiltins ) ;
180183 using var vm = new Rvm ( ) ;
@@ -183,11 +186,161 @@ public void RegisteredHostAwait_RunToCompletion_WithPreloadedResponses()
183186 vm . SetInputJson ( "{\" lang\" : \" es\" }" ) ;
184187
185188 // Pre-load a response for translate
186- vm . SetHostAwaitResponses ( "translate" , new [ ] { "\" hola\" " } ) ;
189+ vm . SetHostAwaitResponses ( new Dictionary < string , IReadOnlyList < string > >
190+ {
191+ [ "translate" ] = new [ ] { "\" hola\" " } ,
192+ } ) ;
187193
188194 // Execute — translate returns "hola"
189195 var result = vm . Execute ( ) ;
190196 Assert . AreEqual ( "\" hola\" " , result , "expected greeting=hola" ) ;
191197 }
192198
199+ [ TestMethod ]
200+ public void RegisteredHostAwait_CompileRejectsEmptyOrWhitespaceName ( )
201+ {
202+ var modules = new [ ] { new PolicyModule ( "noop.rego" , "package demo\n allow := true\n " ) } ;
203+ var entryPoints = new [ ] { "data.demo.allow" } ;
204+
205+ foreach ( var badName in new [ ] { "" , " " , "\t " } )
206+ {
207+ var builtins = new [ ] { new HostAwaitBuiltin ( badName ) } ;
208+ Assert . ThrowsException < InvalidOperationException > (
209+ ( ) => Program . CompileFromModules ( "{}" , modules , entryPoints , builtins ) ,
210+ $ "expected compilation to reject empty/whitespace name '{ badName } '") ;
211+ }
212+ }
213+
214+ [ TestMethod ]
215+ public void RegisteredHostAwait_CompileRejectsDuplicateRegistration ( )
216+ {
217+ var modules = new [ ] { new PolicyModule ( "noop.rego" , "package demo\n allow := true\n " ) } ;
218+ var entryPoints = new [ ] { "data.demo.allow" } ;
219+ var builtins = new [ ]
220+ {
221+ new HostAwaitBuiltin ( "translate" ) ,
222+ new HostAwaitBuiltin ( "translate" ) ,
223+ } ;
224+
225+ Assert . ThrowsException < InvalidOperationException > (
226+ ( ) => Program . CompileFromModules ( "{}" , modules , entryPoints , builtins ) ,
227+ "expected compilation to reject duplicate registration" ) ;
228+ }
229+
230+ [ TestMethod ]
231+ public void RegisteredHostAwait_CompileRejectsReservedName ( )
232+ {
233+ var modules = new [ ] { new PolicyModule ( "noop.rego" , "package demo\n allow := true\n " ) } ;
234+ var entryPoints = new [ ] { "data.demo.allow" } ;
235+ var builtins = new [ ] { new HostAwaitBuiltin ( "__builtin_host_await" ) } ;
236+
237+ Assert . ThrowsException < InvalidOperationException > (
238+ ( ) => Program . CompileFromModules ( "{}" , modules , entryPoints , builtins ) ,
239+ "expected compilation to reject reserved __builtin_host_await identifier" ) ;
240+ }
241+
242+ [ TestMethod ]
243+ public void RegisteredHostAwait_GetAccessorsReturnNullWhenVmIsNotSuspended ( )
244+ {
245+ var modules = new [ ] { new PolicyModule ( "translate.rego" , TranslatePolicy ) } ;
246+ var entryPoints = new [ ] { "data.demo.greeting" } ;
247+ var hostAwaitBuiltins = new [ ] { new HostAwaitBuiltin ( "translate" ) } ;
248+
249+ using var program = Program . CompileFromModules ( "{}" , modules , entryPoints , hostAwaitBuiltins ) ;
250+ using var vm = new Rvm ( ) ;
251+ vm . SetExecutionMode ( ExecutionMode . RunToCompletion ) ;
252+ vm . LoadProgram ( program ) ;
253+ vm . SetInputJson ( "{\" lang\" : \" es\" }" ) ;
254+ vm . SetHostAwaitResponses ( new Dictionary < string , IReadOnlyList < string > >
255+ {
256+ [ "translate" ] = new [ ] { "\" hola\" " } ,
257+ } ) ;
258+ vm . Execute ( ) ;
259+
260+ // After run-to-completion completes successfully, the VM is no longer suspended.
261+ Assert . IsNull ( vm . GetHostAwaitArgument ( ) , "expected null argument when VM is not suspended" ) ;
262+ Assert . IsNull ( vm . GetHostAwaitIdentifier ( ) , "expected null identifier when VM is not suspended" ) ;
263+ }
264+
265+ private const string TranslateNoDefaultPolicy = """
266+ package demo
267+ import rego.v1
268+
269+ # No default — if translate() can't produce a value, the entry point
270+ # evaluation propagates the error to the caller.
271+ result := translate(input.lang)
272+ """ ;
273+
274+ [ TestMethod ]
275+ public void RegisteredHostAwait_RunToCompletion_FailsWhenResponseQueueExhausted ( )
276+ {
277+ var modules = new [ ] { new PolicyModule ( "translate.rego" , TranslateNoDefaultPolicy ) } ;
278+ var entryPoints = new [ ] { "data.demo.result" } ;
279+ var hostAwaitBuiltins = new [ ] { new HostAwaitBuiltin ( "translate" ) } ;
280+
281+ using var program = Program . CompileFromModules ( "{}" , modules , entryPoints , hostAwaitBuiltins ) ;
282+ using var vm = new Rvm ( ) ;
283+ vm . SetExecutionMode ( ExecutionMode . RunToCompletion ) ;
284+ vm . LoadProgram ( program ) ;
285+ vm . SetInputJson ( "{\" lang\" : \" es\" }" ) ;
286+
287+ // No responses pre-loaded — translate has nothing to return.
288+ // Document the actual behavior: in run-to-completion mode the
289+ // missing-response error fails the rule body silently rather than
290+ // surfacing as an exception, so Execute() returns the literal
291+ // string `"<undefined>"` for an entry point that produced no value.
292+ // Asserting the exact return value locks this contract so any
293+ // future change (e.g. propagating an exception) shows up as a
294+ // test failure that has to be explicitly re-acknowledged.
295+ var actual = vm . Execute ( ) ;
296+ Assert . AreEqual (
297+ "\" <undefined>\" " ,
298+ actual ,
299+ "expected `\" <undefined>\" ` when the response queue is exhausted" ) ;
300+ }
301+
302+ private const string MultiAwaitPolicy = """
303+ package demo
304+ import rego.v1
305+
306+ default greeting := "unknown"
307+
308+ greeting := combined if {
309+ hello := translate(input.lang)
310+ user := lookup_user({"id": input.user_id})
311+ combined := sprintf("%s %s", [hello, user.name])
312+ }
313+ """ ;
314+
315+ [ TestMethod ]
316+ public void RegisteredHostAwait_RunToCompletion_MultipleIdentifiersInSingleCall ( )
317+ {
318+ var modules = new [ ] { new PolicyModule ( "multi.rego" , MultiAwaitPolicy ) } ;
319+ var entryPoints = new [ ] { "data.demo.greeting" } ;
320+ var hostAwaitBuiltins = new [ ]
321+ {
322+ new HostAwaitBuiltin ( "translate" ) ,
323+ new HostAwaitBuiltin ( "lookup_user" ) ,
324+ } ;
325+
326+ using var program = Program . CompileFromModules ( "{}" , modules , entryPoints , hostAwaitBuiltins ) ;
327+ using var vm = new Rvm ( ) ;
328+ vm . SetExecutionMode ( ExecutionMode . RunToCompletion ) ;
329+ vm . LoadProgram ( program ) ;
330+ vm . SetInputJson ( "{\" lang\" : \" es\" , \" user_id\" : \" u1\" }" ) ;
331+
332+ // Pre-load responses for BOTH identifiers in a single call.
333+ // The new IReadOnlyDictionary API atomically replaces ALL prior
334+ // responses, so this single call must carry every identifier the
335+ // policy may invoke during this run.
336+ vm . SetHostAwaitResponses ( new Dictionary < string , IReadOnlyList < string > >
337+ {
338+ [ "translate" ] = new [ ] { "\" hola\" " } ,
339+ [ "lookup_user" ] = new [ ] { "{\" name\" : \" Alice\" }" } ,
340+ } ) ;
341+
342+ var result = vm . Execute ( ) ;
343+ Assert . AreEqual ( "\" hola Alice\" " , result , "expected combined greeting from both responses" ) ;
344+ }
345+
193346}
0 commit comments