1- import { describe , it } from "vitest" ;
1+ import { describe , expect , it } from "vitest" ;
22import { Flow } from "@code0-tech/sagittarius-graphql-types" ;
33import { getSignatureSchema , getTypeSchema } from "../../src" ;
44import { DATA_TYPES , FUNCTION_SIGNATURES } from "../data" ;
@@ -276,4 +276,285 @@ describe("Schema", () => {
276276 //console.dir(result, {depth: null})
277277 } ) ;
278278
279+ it ( 'merges function-typed select suggestions with concrete node value' , ( ) => {
280+ // http_method has function-declared type HTTP_METHOD ('GET' | 'POST' | ...).
281+ // Node value "GET" must not collapse the select suggestions to just ['GET'].
282+ const flow : Flow = {
283+ id : "gid://sagittarius/Flow/1" ,
284+ startingNodeId : "gid://sagittarius/NodeFunction/1" ,
285+ signature : "(): void" ,
286+ nodes : {
287+ nodes : [
288+ {
289+ id : "gid://sagittarius/NodeFunction/1" ,
290+ functionDefinition : { identifier : "http::request::send" } ,
291+ parameters : {
292+ nodes : [
293+ { value : { __typename : "LiteralValue" , value : "GET" } } ,
294+ { value : { __typename : "LiteralValue" , value : "/x" } } ,
295+ { value : null } ,
296+ { value : null } ,
297+ { value : null } ,
298+ { value : null } ,
299+ { value : null } ,
300+ { value : null } ,
301+ ] ,
302+ } ,
303+ } ,
304+ ] ,
305+ } ,
306+ } ;
307+
308+ const [ first ] = getSignatureSchema (
309+ flow ,
310+ DATA_TYPES ,
311+ FUNCTION_SIGNATURES ,
312+ "gid://sagittarius/NodeFunction/1" ,
313+ ) ;
314+
315+ expect ( first . schema . input ) . toBe ( "select" ) ;
316+ const values = ( first . schema . suggestions ?? [ ] ) . map ( ( s : any ) => s . value ) ;
317+ expect ( values ) . toEqual (
318+ expect . arrayContaining ( [ "GET" , "POST" , "PUT" , "DELETE" , "PATCH" , "HEAD" ] ) ,
319+ ) ;
320+ } ) ;
321+
322+ it ( 'return: ReferenceValue (payload) vs LiteralValue feeding a generic T parameter' , ( ) => {
323+ // Two-node flow:
324+ // node 1: http::request::send → returns HTTP_RESPONSE<any> ({ payload, headers, http_status_code }).
325+ // node 2: std::control::return<T>(value: T)
326+ // We probe node 2's parameter schema once with a ReferenceValue that pulls
327+ // the .payload of node 1, and once with a LiteralValue. Both feed the same
328+ // generic T parameter, so the schema's structural shape is driven by the
329+ // node side (function side is generic).
330+ const buildFlow = ( returnValue : any ) : Flow => ( {
331+ id : "gid://sagittarius/Flow/1" ,
332+ startingNodeId : "gid://sagittarius/NodeFunction/1" ,
333+ signature : "(): void" ,
334+ nodes : {
335+ nodes : [
336+ {
337+ id : "gid://sagittarius/NodeFunction/1" ,
338+ functionDefinition : { identifier : "http::request::send" } ,
339+ nextNodeId : "gid://sagittarius/NodeFunction/2" ,
340+ parameters : {
341+ nodes : [
342+ { value : { __typename : "LiteralValue" , value : "GET" } } ,
343+ { value : { __typename : "LiteralValue" , value : "/x" } } ,
344+ { value : null } ,
345+ { value : null } ,
346+ { value : null } ,
347+ { value : null } ,
348+ { value : null } ,
349+ { value : null } ,
350+ ] ,
351+ } ,
352+ } ,
353+ {
354+ id : "gid://sagittarius/NodeFunction/2" ,
355+ functionDefinition : { identifier : "std::control::return" } ,
356+ parameters : {
357+ nodes : [ { value : returnValue } ] ,
358+ } ,
359+ } ,
360+ ] ,
361+ } ,
362+ } ) ;
363+
364+ const refFlow = buildFlow ( {
365+ __typename : "ReferenceValue" ,
366+ nodeFunctionId : "gid://sagittarius/NodeFunction/1" ,
367+ referencePath : [ { path : "payload" } ] ,
368+ } ) ;
369+ const literalFlow = buildFlow ( {
370+ __typename : "LiteralValue" ,
371+ value : "hello" ,
372+ } ) ;
373+
374+ const [ refResult ] = getSignatureSchema (
375+ refFlow ,
376+ DATA_TYPES ,
377+ FUNCTION_SIGNATURES ,
378+ "gid://sagittarius/NodeFunction/2" ,
379+ ) ;
380+ const [ literalResult ] = getSignatureSchema (
381+ literalFlow ,
382+ DATA_TYPES ,
383+ FUNCTION_SIGNATURES ,
384+ "gid://sagittarius/NodeFunction/2" ,
385+ ) ;
386+
387+ // Function side is generic for both. The node side decides the structural shape.
388+
389+ // ReferenceValue → node value resolves to HTTP_RESPONSE<any>.payload = any.
390+ // `any` carries no structural info but its runtime shape is open — surface it
391+ // as a `data` schema (open object) rather than the dead-end `generic`.
392+ expect ( refResult . schema . input ) . toBe ( "data" ) ;
393+
394+ // Every reference in scope (node 1 with each of its response paths, plus
395+ // top-level node/flow references and zero-arg node-function nodes) is
396+ // assignable to `any`, so all of them must be offered as suggestions.
397+ const refSuggestions = ( refResult . schema . suggestions ?? [ ] ) as any [ ] ;
398+ expect ( refSuggestions . length ) . toBeGreaterThan ( 0 ) ;
399+ expect (
400+ refSuggestions . some (
401+ ( s ) =>
402+ s . __typename === "ReferenceValue" &&
403+ s . nodeFunctionId === "gid://sagittarius/NodeFunction/1" &&
404+ Array . isArray ( s . referencePath ) &&
405+ s . referencePath . some ( ( p : any ) => p . path === "payload" ) ,
406+ ) ,
407+ ) . toBe ( true ) ;
408+ expect (
409+ refSuggestions . some (
410+ ( s ) =>
411+ s . __typename === "ReferenceValue" &&
412+ s . nodeFunctionId === "gid://sagittarius/NodeFunction/1" &&
413+ Array . isArray ( s . referencePath ) &&
414+ s . referencePath . some ( ( p : any ) => p . path === "headers" ) ,
415+ ) ,
416+ ) . toBe ( true ) ;
417+ expect (
418+ refSuggestions . some (
419+ ( s ) =>
420+ s . __typename === "ReferenceValue" &&
421+ s . nodeFunctionId === "gid://sagittarius/NodeFunction/1" &&
422+ Array . isArray ( s . referencePath ) &&
423+ s . referencePath . some ( ( p : any ) => p . path === "http_status_code" ) ,
424+ ) ,
425+ ) . toBe ( true ) ;
426+
427+ // LiteralValue "hello" → narrows T to the string literal "hello". The node
428+ // schema alone would be a select with one option; the merge must demote it
429+ // to free-form text because the function side is generic.
430+ expect ( literalResult . schema . input ) . toBe ( "text" ) ;
431+
432+ const literalSuggestions = ( literalResult . schema . suggestions ?? [ ] ) as any [ ] ;
433+
434+ // Suggestions are scoped by the *function* side (T → any), not by the narrow
435+ // "hello" literal. Every response field of node 1 is reachable, not just the
436+ // payload that happens to be `any`-typed.
437+ for ( const expectedPath of [ "payload" , "headers" , "http_status_code" ] ) {
438+ expect (
439+ literalSuggestions . some (
440+ ( s ) =>
441+ s . __typename === "ReferenceValue" &&
442+ s . nodeFunctionId === "gid://sagittarius/NodeFunction/1" &&
443+ Array . isArray ( s . referencePath ) &&
444+ s . referencePath . some ( ( p : any ) => p . path === expectedPath ) ,
445+ ) ,
446+ ) . toBe ( true ) ;
447+ }
448+ } ) ;
449+
450+ it ( 'generic function parameter gives the same suggestion set regardless of the current literal kind' , ( ) => {
451+ // `std::control::return<T>(value: T)` accepts anything. The set of
452+ // suggestions the user can pick from should not shrink just because the
453+ // currently-set value happens to narrow T (e.g. to `boolean`).
454+ const buildFlow = ( returnValue : any ) : Flow => ( {
455+ id : "gid://sagittarius/Flow/1" ,
456+ startingNodeId : "gid://sagittarius/NodeFunction/1" ,
457+ signature : "(): void" ,
458+ nodes : {
459+ nodes : [
460+ {
461+ id : "gid://sagittarius/NodeFunction/1" ,
462+ functionDefinition : { identifier : "http::request::send" } ,
463+ nextNodeId : "gid://sagittarius/NodeFunction/2" ,
464+ parameters : {
465+ nodes : [
466+ { value : { __typename : "LiteralValue" , value : "GET" } } ,
467+ { value : { __typename : "LiteralValue" , value : "/x" } } ,
468+ { value : null } ,
469+ { value : null } ,
470+ { value : null } ,
471+ { value : null } ,
472+ { value : null } ,
473+ { value : null } ,
474+ ] ,
475+ } ,
476+ } ,
477+ {
478+ id : "gid://sagittarius/NodeFunction/2" ,
479+ functionDefinition : { identifier : "std::control::return" } ,
480+ parameters : {
481+ nodes : [ { value : returnValue } ] ,
482+ } ,
483+ } ,
484+ ] ,
485+ } ,
486+ } ) ;
487+
488+ const probe = ( returnValue : any ) => {
489+ const [ r ] = getSignatureSchema (
490+ buildFlow ( returnValue ) ,
491+ DATA_TYPES ,
492+ FUNCTION_SIGNATURES ,
493+ "gid://sagittarius/NodeFunction/2" ,
494+ ) ;
495+ return ( ( r . schema . suggestions ?? [ ] ) as any [ ] ) . map ( ( s ) =>
496+ JSON . stringify ( s ) ,
497+ ) ;
498+ } ;
499+
500+ // Snapshot the set of references/node-functions surfaced for each value
501+ // kind. Strip literal-value suggestions because those legitimately differ
502+ // (the empty-object case has no literal alternative; the boolean case
503+ // would surface `true`/`false` via getValues of the booleans constraint).
504+ const onlyRefsAndNodes = ( sigs : string [ ] ) =>
505+ sigs . filter ( ( s ) => ! s . includes ( '"LiteralValue"' ) ) ;
506+
507+ const objectSet = onlyRefsAndNodes (
508+ probe ( { __typename : "LiteralValue" , value : { } } ) ,
509+ ) ;
510+ const boolSet = onlyRefsAndNodes (
511+ probe ( { __typename : "LiteralValue" , value : true } ) ,
512+ ) ;
513+ const stringSet = onlyRefsAndNodes (
514+ probe ( { __typename : "LiteralValue" , value : "x" } ) ,
515+ ) ;
516+ const numberSet = onlyRefsAndNodes (
517+ probe ( { __typename : "LiteralValue" , value : 42 } ) ,
518+ ) ;
519+
520+ // All sets are non-empty (the function accepts anything → references in
521+ // scope are valid candidates) and identical to each other.
522+ expect ( objectSet . length ) . toBeGreaterThan ( 0 ) ;
523+ expect ( new Set ( boolSet ) ) . toEqual ( new Set ( objectSet ) ) ;
524+ expect ( new Set ( stringSet ) ) . toEqual ( new Set ( objectSet ) ) ;
525+ expect ( new Set ( numberSet ) ) . toEqual ( new Set ( objectSet ) ) ;
526+ } ) ;
527+
528+ it ( 'demotes select to free-form when function parameter is generic' , ( ) => {
529+ // std::control::return<T>(value: T): T — function declares T, node sets a
530+ // string literal. Result must be text (free-form), NOT select with one option.
531+ const flow : Flow = {
532+ id : "gid://sagittarius/Flow/2" ,
533+ startingNodeId : "gid://sagittarius/NodeFunction/1" ,
534+ signature : "(): void" ,
535+ nodes : {
536+ nodes : [
537+ {
538+ id : "gid://sagittarius/NodeFunction/1" ,
539+ functionDefinition : { identifier : "std::control::return" } ,
540+ parameters : {
541+ nodes : [
542+ { value : { __typename : "LiteralValue" , value : "Test" } } ,
543+ ] ,
544+ } ,
545+ } ,
546+ ] ,
547+ } ,
548+ } ;
549+
550+ const [ first ] = getSignatureSchema (
551+ flow ,
552+ DATA_TYPES ,
553+ FUNCTION_SIGNATURES ,
554+ "gid://sagittarius/NodeFunction/1" ,
555+ ) ;
556+
557+ expect ( first . schema . input ) . toBe ( "text" ) ;
558+ } ) ;
559+
279560} )
0 commit comments