@@ -831,6 +831,155 @@ describe("graph diagnostics", () => {
831831 expect ( diags [ 0 ] ?. semanticNodeId ) . toBe ( "action:hidden" )
832832 } )
833833 } )
834+
835+ describe ( "flow-step-not-surfaced diagnostic" , ( ) => {
836+ it ( "does not emit flow-step-not-surfaced when all flow steps are surfaced" , ( ) => {
837+ const screenDef = screen ( "AllStepsSurfaced" , $ => {
838+ const email = $ . state . text ( "email" )
839+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
840+ const login = $ . act ( "Log in" )
841+ $ . surface ( "main" ) . contains ( emailAsk , login )
842+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( login )
843+ } )
844+ const inspected = inspectScreen ( screenDef )
845+ const diags = inspected . diagnostics . filter ( d => d . code === "flow-step-not-surfaced" )
846+ expect ( diags ) . toHaveLength ( 0 )
847+ } )
848+
849+ it ( "emits flow-step-not-surfaced for unsurfaced ask step in flow" , ( ) => {
850+ const screenDef = screen ( "UnsurfacedAskInFlow" , $ => {
851+ const email = $ . state . text ( "email" )
852+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
853+ const login = $ . act ( "Log in" )
854+ $ . surface ( "main" ) . contains ( login )
855+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( login )
856+ } )
857+ const inspected = inspectScreen ( screenDef )
858+ const diags = inspected . diagnostics . filter ( d => d . code === "flow-step-not-surfaced" )
859+ expect ( diags ) . toHaveLength ( 1 )
860+ expect ( diags [ 0 ] ?. severity ) . toBe ( "warning" )
861+ expect ( diags [ 0 ] ?. nodeId ) . toBe ( "ask_email" )
862+ } )
863+
864+ it ( "emits flow-step-not-surfaced for unsurfaced action step in flow" , ( ) => {
865+ const screenDef = screen ( "UnsurfacedActInFlow" , $ => {
866+ const email = $ . state . text ( "email" )
867+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
868+ const login = $ . act ( "Log in" )
869+ $ . surface ( "main" ) . contains ( emailAsk )
870+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( login )
871+ } )
872+ const inspected = inspectScreen ( screenDef )
873+ const diags = inspected . diagnostics . filter ( d => d . code === "flow-step-not-surfaced" )
874+ expect ( diags ) . toHaveLength ( 1 )
875+ expect ( diags [ 0 ] ?. severity ) . toBe ( "warning" )
876+ expect ( diags [ 0 ] ?. nodeId ) . toBe ( "act_log_in" )
877+ } )
878+
879+ it ( "only reports unsurfaced steps when some are surfaced and some are not" , ( ) => {
880+ const screenDef = screen ( "MixedSurfaced" , $ => {
881+ const email = $ . state . text ( "email" )
882+ const pwd = $ . state . text ( "password" )
883+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
884+ const pwdAsk = $ . ask ( "Password" , pwd ) . private ( )
885+ const login = $ . act ( "Log in" )
886+ $ . surface ( "main" ) . contains ( emailAsk , login )
887+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( pwdAsk ) . then ( login )
888+ } )
889+ const inspected = inspectScreen ( screenDef )
890+ const diags = inspected . diagnostics . filter ( d => d . code === "flow-step-not-surfaced" )
891+ expect ( diags ) . toHaveLength ( 1 )
892+ expect ( diags [ 0 ] ?. nodeId ) . toBe ( "ask_password" )
893+ } )
894+
895+ it ( "does not emit flow-step-not-surfaced when there are no flows" , ( ) => {
896+ const screenDef = screen ( "NoFlowsAtAll" , $ => {
897+ const email = $ . state . text ( "email" )
898+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
899+ const login = $ . act ( "Log in" )
900+ $ . surface ( "main" ) . contains ( emailAsk , login )
901+ } )
902+ const inspected = inspectScreen ( screenDef )
903+ const diags = inspected . diagnostics . filter ( d => d . code === "flow-step-not-surfaced" )
904+ expect ( diags ) . toHaveLength ( 0 )
905+ } )
906+
907+ it ( "emits deterministic flow-step-not-surfaced diagnostics across multiple flows" , ( ) => {
908+ const screenDef = screen ( "MultipleFlowsDet" , $ => {
909+ const email = $ . state . text ( "email" )
910+ const pwd = $ . state . text ( "password" )
911+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
912+ const pwdAsk = $ . ask ( "Password" , pwd ) . private ( )
913+ const login = $ . act ( "Log in" )
914+ const signup = $ . act ( "Sign up" )
915+ $ . surface ( "main" ) . contains ( emailAsk , login , signup )
916+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( pwdAsk ) . then ( login )
917+ $ . flow ( "register" ) . startsWith ( pwdAsk ) . then ( signup )
918+ } )
919+ const inspected = inspectScreen ( screenDef )
920+ const diags = inspected . diagnostics . filter ( d => d . code === "flow-step-not-surfaced" )
921+ expect ( diags ) . toHaveLength ( 2 )
922+ expect ( diags [ 0 ] ?. nodeId ) . toBe ( "ask_password" )
923+ expect ( diags [ 0 ] ?. flow ?. flowNodeId ) . toBe ( "flow_login" )
924+ expect ( diags [ 1 ] ?. nodeId ) . toBe ( "ask_password" )
925+ expect ( diags [ 1 ] ?. flow ?. flowNodeId ) . toBe ( "flow_register" )
926+ } )
927+
928+ it ( "includes nodeId and semanticNodeId for the unsurfaced step" , ( ) => {
929+ const screenDef = screen ( "WithSemanticStep" , $ => {
930+ const email = $ . state . text ( "email" )
931+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
932+ const login = $ . act ( "Log in" )
933+ $ . surface ( "main" ) . contains ( login )
934+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( login )
935+ } )
936+ const inspected = inspectScreen ( screenDef )
937+ const diags = inspected . diagnostics . filter ( d => d . code === "flow-step-not-surfaced" )
938+ expect ( diags ) . toHaveLength ( 1 )
939+ expect ( diags [ 0 ] ?. nodeId ) . toBe ( "ask_email" )
940+ expect ( diags [ 0 ] ?. semanticNodeId ) . toBe ( "ask:email" )
941+ } )
942+
943+ it ( "includes nested flow.flowNodeId and flow.flowSemanticNodeId" , ( ) => {
944+ const screenDef = screen ( "NestedFlowMeta" , $ => {
945+ const email = $ . state . text ( "email" )
946+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
947+ const login = $ . act ( "Log in" )
948+ $ . surface ( "main" ) . contains ( login )
949+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( login )
950+ } )
951+ const inspected = inspectScreen ( screenDef )
952+ const diags = inspected . diagnostics . filter ( d => d . code === "flow-step-not-surfaced" )
953+ expect ( diags ) . toHaveLength ( 1 )
954+ expect ( diags [ 0 ] ?. flow ?. flowNodeId ) . toBe ( "flow_login" )
955+ expect ( diags [ 0 ] ?. flow ?. flowSemanticNodeId ) . toBe ( "flow:login" )
956+ } )
957+
958+ it ( "coexists with surfaced-node-not-in-any-flow without breaking determinism" , ( ) => {
959+ const screenDef = screen ( "CoexistFlow" , $ => {
960+ const email = $ . state . text ( "email" )
961+ const pwd = $ . state . text ( "password" )
962+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
963+ const pwdAsk = $ . ask ( "Password" , pwd ) . private ( )
964+ const login = $ . act ( "Log in" ) . primary ( )
965+ const signup = $ . act ( "Sign up" ) . primary ( )
966+ $ . surface ( "main" ) . contains ( emailAsk , login , signup )
967+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( pwdAsk ) . then ( login )
968+ } )
969+ const first = inspectScreen ( screenDef )
970+ const second = inspectScreen ( screenDef )
971+ expect ( first . diagnostics . map ( d => d . code ) ) . toEqual ( second . diagnostics . map ( d => d . code ) )
972+ const codes = first . diagnostics . map ( d => d . code )
973+ expect ( codes ) . toContain ( "flow-step-not-surfaced" )
974+ expect ( codes ) . toContain ( "surfaced-node-not-in-any-flow" )
975+ const flowDiags = first . diagnostics . filter ( d => d . code === "flow-step-not-surfaced" )
976+ expect ( flowDiags ) . toHaveLength ( 1 )
977+ expect ( flowDiags [ 0 ] ?. nodeId ) . toBe ( "ask_password" )
978+ const reachDiags = first . diagnostics . filter ( d => d . code === "surfaced-node-not-in-any-flow" )
979+ expect ( reachDiags ) . toHaveLength ( 1 )
980+ expect ( reachDiags [ 0 ] ?. nodeId ) . toBe ( "act_sign_up" )
981+ } )
982+ } )
834983} )
835984
836985describe ( "resource" , ( ) => {
0 commit comments