From 130e88716c704922886eff1d97038a7bb7506577 Mon Sep 17 00:00:00 2001 From: Nick Sutterer Date: Thu, 15 Feb 2024 16:25:44 +0100 Subject: [PATCH] introduce discovery_test, finally. --- lib/trailblazer/workflow/discovery.rb | 30 +++--- test/collaboration_test.rb | 148 +------------------------- test/discovery_test.rb | 56 ++++++++++ test/test_helper.rb | 148 ++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 159 deletions(-) create mode 100644 test/discovery_test.rb diff --git a/lib/trailblazer/workflow/discovery.rb b/lib/trailblazer/workflow/discovery.rb index 9f61552..a0bac09 100644 --- a/lib/trailblazer/workflow/discovery.rb +++ b/lib/trailblazer/workflow/discovery.rb @@ -20,7 +20,7 @@ def call(collaboration, start_position:, run_multiple_times: {}, initial_lane_po # imply we start from a public resume and discover the path? # we could save work on {run_multiple_times} with this. - collaboration, message_flow, start_position, initial_lane_positions = stub_tasks_for(collaboration, message_flow: message_flow, start_position: start_position, initial_lane_positions: initial_lane_positions) + collaboration, message_flow, start_position, initial_lane_positions, original_activity_2_stub_activity, original_task_2_stub_task = stub_tasks_for(collaboration, message_flow: message_flow, start_position: start_position, initial_lane_positions: initial_lane_positions) # pp collaboration.to_h[:lanes][:ui].to_h # raise @@ -34,7 +34,7 @@ def call(collaboration, start_position:, run_multiple_times: {}, initial_lane_po ] ] - states = [] + discovered_states = [] additional_state_data = {} already_visited_catch_events = {} @@ -60,7 +60,7 @@ def call(collaboration, start_position:, run_multiple_times: {}, initial_lane_po # register new state. # Note that we do that before anything is invoked. - states << state = [lane_positions, start_position] # FIXME: we need to add {configuration} here! + discovered_states << state = [lane_positions, start_position] # FIXME: we need to add {configuration} here! state_data = [ctx.inspect] @@ -114,14 +114,14 @@ def call(collaboration, start_position:, run_multiple_times: {}, initial_lane_po end end - # {states} is compile-time relevant + # {discovered_states} is compile-time relevant # {additional_state_data} is runtime - return states, additional_state_data + return discovered_states, additional_state_data end def stub_tasks_for(collaboration, ignore_class: Trailblazer::Activity::End, message_flow:, start_position:, initial_lane_positions:) - stubbed_lanes = collaboration.to_h[:lanes].collect do |lane_id, activity| + collected = collaboration.to_h[:lanes].collect do |lane_id, activity| circuit = activity.to_h[:circuit] lane_map = circuit.to_h[:map].clone @@ -172,23 +172,27 @@ def stub_tasks_for(collaboration, ignore_class: Trailblazer::Activity::End, mess lane = Activity.new(Activity::Schema.new(new_circuit, activity.to_h[:outputs], new_nodes, activity.to_h[:config])) # FIXME: breaking taskWrap here (which is no problem, actually). # [lane_id, lane, replaced_tasks] - [lane_id, lane] - end.to_h + [[lane_id, lane], replaced_tasks] + end + + original_task_2_stub_task = collected.inject({}) { |memo, (_, replaced_tasks)| memo.merge(replaced_tasks) } + + stubbed_lanes = collected.collect { |lane, _| lane }.to_h - old_activity_2_new_activity = collaboration.to_h[:lanes].collect { |lane_id, activity| [activity, stubbed_lanes[lane_id]] }.to_h + original_activity_2_stub_activity = collaboration.to_h[:lanes].collect { |lane_id, activity| [activity, stubbed_lanes[lane_id]] }.to_h - new_message_flow = message_flow.collect { |throw_evt, (activity, catch_evt)| [throw_evt, [old_activity_2_new_activity[activity], catch_evt]] }.to_h + new_message_flow = message_flow.collect { |throw_evt, (activity, catch_evt)| [throw_evt, [original_activity_2_stub_activity[activity], catch_evt]] }.to_h - new_start_position = Collaboration::Position.new(old_activity_2_new_activity.fetch(start_position.activity), start_position.task) + new_start_position = Collaboration::Position.new(original_activity_2_stub_activity.fetch(start_position.activity), start_position.task) new_initial_lane_positions = initial_lane_positions.collect do |position| # TODO: make lane_positions {Position} instances, too. - Collaboration::Position.new(old_activity_2_new_activity[position[0]], position[1]) + Collaboration::Position.new(original_activity_2_stub_activity[position[0]], position[1]) end new_initial_lane_positions = Collaboration::Positions.new(new_initial_lane_positions) - return Collaboration::Schema.new(lanes: stubbed_lanes, message_flow: new_message_flow), new_message_flow, new_start_position, new_initial_lane_positions + return Collaboration::Schema.new(lanes: stubbed_lanes, message_flow: new_message_flow), new_message_flow, new_start_position, new_initial_lane_positions, original_activity_2_stub_activity, original_task_2_stub_task end end end diff --git a/test/collaboration_test.rb b/test/collaboration_test.rb index 044dc42..c72c38a 100644 --- a/test/collaboration_test.rb +++ b/test/collaboration_test.rb @@ -7,152 +7,7 @@ class CollaborationTest < Minitest::Spec include Trailblazer::Test::Assertions # DISCUSS: this is for assert_advance and friends. - def build_schema() - moderation_json = File.read("test/fixtures/v1/moderation.json") - signal, (ctx, _) = Trailblazer::Workflow::Generate.invoke([{json_document: moderation_json}, {}]) - - article_moderation_intermediate = ctx[:intermediates]["article moderation"] - # pp article_moderation_intermediate - - implementing = Trailblazer::Activity::Testing.def_steps(:create, :update, :notify_approver, :reject, :approve, :revise, :publish, :archive, :delete) - - lane_activity = Trailblazer::Workflow::Collaboration.Lane( - article_moderation_intermediate, - - "Create" => implementing.method(:create), - "Update" => implementing.method(:update), - "Approve" => implementing.method(:approve), - "Notify approver" => implementing.method(:notify_approver), - "Revise" => implementing.method(:revise), - "Reject" => implementing.method(:reject), - "Publish" => implementing.method(:publish), - "Archive" => implementing.method(:archive), - "Delete" => implementing.method(:delete), - ) - - - article_moderation_intermediate = ctx[:intermediates][" author workflow"] - # pp article_moderation_intermediate - - implementing = Trailblazer::Activity::Testing.def_steps(:create_form, :ui_create, :update_form, :ui_update, :notify_approver, :reject, :approve, :revise, :publish, :archive, :delete, :delete_form, :cancel, :revise_form, - :create_form_with_errors, :update_form_with_errors, :revise_form_with_errors) - - lane_activity_ui = Trailblazer::Workflow::Collaboration.Lane( - article_moderation_intermediate, - - "Create form" => implementing.method(:create_form), - "Create" => implementing.method(:ui_create), - "Update form" => implementing.method(:update_form), - "Update" => implementing.method(:ui_update), - "Notify approver" => implementing.method(:notify_approver), - "Publish" => implementing.method(:publish), - "Delete" => implementing.method(:delete), - "Delete? form" => implementing.method(:delete_form), - "Cancel" => implementing.method(:cancel), - "Revise" => implementing.method(:revise), - "Revise form" => implementing.method(:revise_form), - "Create form with errors" => implementing.method(:create_form_with_errors), - "Update form with errors" => implementing.method(:update_form_with_errors), - "Revise form with errors" => implementing.method(:revise_form_with_errors), - "Archive" => implementing.method(:archive), - # "Approve" => implementing.method(:approve), - # "Reject" => implementing.method(:reject), - ) - - # TODO: move this into the Schema-build process. - # This is needed to translate the JSON message structure to Ruby, - # where we reference lanes by their {Activity} instance. - json_id_to_lane = { - "article moderation" => lane_activity, - " author workflow" => lane_activity_ui, - } - - # lane_icons: lane_icons = {"UI" => "☝", "lifecycle" => "⛾", "approver" => "☑"}, - lanes_cfg = { - "article moderation" => { - label: "lifecycle", - icon: "⛾", - activity: lane_activity, # this is copied here after the activity has been compiled in {Schema.build}. - }, - " author workflow" => { - label: "UI", - icon: "☝", - activity: lane_activity_ui, - }, - # TODO: add editor/approver lane. - } - - # pp ctx[:structure].lanes - message_flow = Trailblazer::Workflow::Collaboration.Messages( - ctx[:structure].messages, - json_id_to_lane - ) - - # DISCUSS: {lanes} is always ID to activity? - lanes = { - lifecycle: lane_activity, - ui: lane_activity_ui, - } - - approver_activity, extended_message_flow, extended_initial_lane_positions = build_custom_editor_lane(lanes, message_flow) - - lanes = lanes.merge(approver: approver_activity) - - schema = Trailblazer::Workflow::Collaboration::Schema.new( - lanes: lanes, - message_flow: message_flow, - ) - - return schema, lanes, extended_message_flow, extended_initial_lane_positions - end - - # DISCUSS: this is mostly to play around with the "API" of building a Collaboration. - def build_custom_editor_lane(lanes, message_flow) - approve_id = "Activity_1qrkaz0" - reject_id = "Activity_0d9yewp" - - lifecycle_activity = lanes[:lifecycle] - - missing_throw_from_notify_approver = Trailblazer::Activity::Introspect.Nodes(lifecycle_activity, id: "throw-after-Activity_0wr78cv").task - - decision_is_approve_throw = nil - decision_is_reject_throw = nil - - approver_start_suspend = nil - approver_activity = Class.new(Trailblazer::Activity::Railway) do - step task: approver_start_suspend = Trailblazer::Workflow::Event::Suspend.new(semantic: "invented_semantic", "resumes" => ["catch-before-decider-xxx"]) - - fail task: Trailblazer::Workflow::Event::Catch.new(semantic: "xxx --> decider"), id: "catch-before-decider-xxx", Output(:success) => Track(:failure) - fail :decider, id: "xxx", - Output(:failure) => Trailblazer::Activity::Railway.Id("xxx_reject") - fail task: decision_is_approve_throw = Trailblazer::Workflow::Event::Throw.new(semantic: "xxx_approve") - - step task: decision_is_reject_throw = Trailblazer::Workflow::Event::Throw.new(semantic: "xxx_reject"), - magnetic_to: :reject, id: "xxx_reject" - - def decider(ctx, decision: true, **) - # raise if !decision - - decision - end - end - - extended_message_flow = message_flow.merge( - # "throw-after-Activity_0wr78cv" - missing_throw_from_notify_approver => [approver_activity, Trailblazer::Activity::Introspect.Nodes(approver_activity, id: "catch-before-decider-xxx").task], - decision_is_approve_throw => [lifecycle_activity, Trailblazer::Activity::Introspect.Nodes(lifecycle_activity, id: "catch-before-#{approve_id}").task], - decision_is_reject_throw => [lifecycle_activity, Trailblazer::Activity::Introspect.Nodes(lifecycle_activity, id: "catch-before-#{reject_id}").task], - ) - - initial_lane_positions = Trailblazer::Workflow::Collaboration::Synchronous.initial_lane_positions(lanes) # we need to do this manually here, as initial_lane_positions isn't part of the {Schema.build} process. - extended_initial_lane_positions = initial_lane_positions.merge( - approver_activity => approver_start_suspend - ) - extended_initial_lane_positions = Trailblazer::Workflow::Collaboration::Positions.new(extended_initial_lane_positions.collect { |activity, task| Trailblazer::Workflow::Collaboration::Position.new(activity, task) }) - - return approver_activity, extended_message_flow, extended_initial_lane_positions - end - + include BuildSchema # TODO: remove me, or move me at least! # DISCUSS: {states} should probably be named {reached_states} as some states appear multiple times in the list. @@ -295,6 +150,7 @@ def render_states(states, lanes:, additional_state_data:, task_map:) start_position: start_position, message_flow: message_flow, + # TODO: allow translating the original "id" (?) to the stubbed. run_multiple_times: { # We're "clicking" the [Notify_approver] button again, this time to get rejected. Trailblazer::Activity::Introspect.Nodes(lane_activity_ui, id: "catch-before-#{ui_notify_approver}").task => {ctx_merge: { diff --git a/test/discovery_test.rb b/test/discovery_test.rb new file mode 100644 index 0000000..0dcad8c --- /dev/null +++ b/test/discovery_test.rb @@ -0,0 +1,56 @@ +require "test_helper" + +class DiscoveryTest < Minitest::Spec + include BuildSchema + + it "Discovery.call" do + ui_create_form = "Activity_0wc2mcq" # TODO: this is from pro-rails tests. + ui_create = "Activity_1psp91r" + ui_update = "Activity_0j78uzd" + ui_notify_approver = "Activity_1dt5di5" + + schema, lanes, message_flow, initial_lane_positions = build_schema() + + lane_activity = lanes[:lifecycle] + lane_activity_ui = lanes[:ui] + approver_activity = lanes[:approver] + + + # TODO: do this in the State layer. + start_task = Trailblazer::Activity::Introspect.Nodes(lane_activity_ui, id: "catch-before-#{ui_create_form}").task # catch-before-Activity_0wc2mcq + start_position = Trailblazer::Workflow::Collaboration::Position.new(lane_activity_ui, start_task) + + + states, additional_state_data = Trailblazer::Workflow::Discovery.( + schema, + initial_lane_positions: initial_lane_positions, + start_position: start_position, + message_flow: message_flow, + + # TODO: allow translating the original "id" (?) to the stubbed. + run_multiple_times: { + # We're "clicking" the [Notify_approver] button again, this time to get rejected. + Trailblazer::Activity::Introspect.Nodes(lane_activity_ui, id: "catch-before-#{ui_notify_approver}").task => {ctx_merge: { + # decision: false, # TODO: this is how it should be. + :"approver:xxx" => Trailblazer::Activity::Left, # FIXME: {:decision} must be translated to {:"approver:xxx"} + }, config_payload: {outcome: :failure}}, + + # Click [UI Create] again, with invalid data. + Trailblazer::Activity::Introspect.Nodes(lane_activity_ui, id: "catch-before-#{ui_create}").task => {ctx_merge: { + # create: false + :"lifecycle:Create" => Trailblazer::Activity::Left, + }, config_payload: {outcome: :failure}}, # lifecycle create is supposed to fail. + + # Click [UI Update] again, with invalid data. + Trailblazer::Activity::Introspect.Nodes(lane_activity_ui, id: "catch-before-#{ui_update}").task => {ctx_merge: { + # update: false + :"lifecycle:Update" => Trailblazer::Activity::Left, + }, config_payload: {outcome: :failure}}, # lifecycle create is supposed to fail. + } + ) + + pp states + + raise + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 2306a9c..b828db9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,3 +12,151 @@ def assert_equal(expected, asserted, *args) super(asserted, expected, *args) end end + +module BuildSchema + def build_schema() + moderation_json = File.read("test/fixtures/v1/moderation.json") + signal, (ctx, _) = Trailblazer::Workflow::Generate.invoke([{json_document: moderation_json}, {}]) + + article_moderation_intermediate = ctx[:intermediates]["article moderation"] + # pp article_moderation_intermediate + + implementing = Trailblazer::Activity::Testing.def_steps(:create, :update, :notify_approver, :reject, :approve, :revise, :publish, :archive, :delete) + + lane_activity = Trailblazer::Workflow::Collaboration.Lane( + article_moderation_intermediate, + + "Create" => implementing.method(:create), + "Update" => implementing.method(:update), + "Approve" => implementing.method(:approve), + "Notify approver" => implementing.method(:notify_approver), + "Revise" => implementing.method(:revise), + "Reject" => implementing.method(:reject), + "Publish" => implementing.method(:publish), + "Archive" => implementing.method(:archive), + "Delete" => implementing.method(:delete), + ) + + + article_moderation_intermediate = ctx[:intermediates][" author workflow"] + # pp article_moderation_intermediate + + implementing = Trailblazer::Activity::Testing.def_steps(:create_form, :ui_create, :update_form, :ui_update, :notify_approver, :reject, :approve, :revise, :publish, :archive, :delete, :delete_form, :cancel, :revise_form, + :create_form_with_errors, :update_form_with_errors, :revise_form_with_errors) + + lane_activity_ui = Trailblazer::Workflow::Collaboration.Lane( + article_moderation_intermediate, + + "Create form" => implementing.method(:create_form), + "Create" => implementing.method(:ui_create), + "Update form" => implementing.method(:update_form), + "Update" => implementing.method(:ui_update), + "Notify approver" => implementing.method(:notify_approver), + "Publish" => implementing.method(:publish), + "Delete" => implementing.method(:delete), + "Delete? form" => implementing.method(:delete_form), + "Cancel" => implementing.method(:cancel), + "Revise" => implementing.method(:revise), + "Revise form" => implementing.method(:revise_form), + "Create form with errors" => implementing.method(:create_form_with_errors), + "Update form with errors" => implementing.method(:update_form_with_errors), + "Revise form with errors" => implementing.method(:revise_form_with_errors), + "Archive" => implementing.method(:archive), + # "Approve" => implementing.method(:approve), + # "Reject" => implementing.method(:reject), + ) + + # TODO: move this into the Schema-build process. + # This is needed to translate the JSON message structure to Ruby, + # where we reference lanes by their {Activity} instance. + json_id_to_lane = { + "article moderation" => lane_activity, + " author workflow" => lane_activity_ui, + } + + # lane_icons: lane_icons = {"UI" => "☝", "lifecycle" => "⛾", "approver" => "☑"}, + lanes_cfg = { + "article moderation" => { + label: "lifecycle", + icon: "⛾", + activity: lane_activity, # this is copied here after the activity has been compiled in {Schema.build}. + }, + " author workflow" => { + label: "UI", + icon: "☝", + activity: lane_activity_ui, + }, + # TODO: add editor/approver lane. + } + + # pp ctx[:structure].lanes + message_flow = Trailblazer::Workflow::Collaboration.Messages( + ctx[:structure].messages, + json_id_to_lane + ) + + # DISCUSS: {lanes} is always ID to activity? + lanes = { + lifecycle: lane_activity, + ui: lane_activity_ui, + } + + approver_activity, extended_message_flow, extended_initial_lane_positions = build_custom_editor_lane(lanes, message_flow) + + lanes = lanes.merge(approver: approver_activity) + + schema = Trailblazer::Workflow::Collaboration::Schema.new( + lanes: lanes, + message_flow: message_flow, + ) + + return schema, lanes, extended_message_flow, extended_initial_lane_positions + end + + # DISCUSS: this is mostly to play around with the "API" of building a Collaboration. + def build_custom_editor_lane(lanes, message_flow) + approve_id = "Activity_1qrkaz0" + reject_id = "Activity_0d9yewp" + + lifecycle_activity = lanes[:lifecycle] + + missing_throw_from_notify_approver = Trailblazer::Activity::Introspect.Nodes(lifecycle_activity, id: "throw-after-Activity_0wr78cv").task + + decision_is_approve_throw = nil + decision_is_reject_throw = nil + + approver_start_suspend = nil + approver_activity = Class.new(Trailblazer::Activity::Railway) do + step task: approver_start_suspend = Trailblazer::Workflow::Event::Suspend.new(semantic: "invented_semantic", "resumes" => ["catch-before-decider-xxx"]) + + fail task: Trailblazer::Workflow::Event::Catch.new(semantic: "xxx --> decider"), id: "catch-before-decider-xxx", Output(:success) => Track(:failure) + fail :decider, id: "xxx", + Output(:failure) => Trailblazer::Activity::Railway.Id("xxx_reject") + fail task: decision_is_approve_throw = Trailblazer::Workflow::Event::Throw.new(semantic: "xxx_approve") + + step task: decision_is_reject_throw = Trailblazer::Workflow::Event::Throw.new(semantic: "xxx_reject"), + magnetic_to: :reject, id: "xxx_reject" + + def decider(ctx, decision: true, **) + # raise if !decision + + decision + end + end + + extended_message_flow = message_flow.merge( + # "throw-after-Activity_0wr78cv" + missing_throw_from_notify_approver => [approver_activity, Trailblazer::Activity::Introspect.Nodes(approver_activity, id: "catch-before-decider-xxx").task], + decision_is_approve_throw => [lifecycle_activity, Trailblazer::Activity::Introspect.Nodes(lifecycle_activity, id: "catch-before-#{approve_id}").task], + decision_is_reject_throw => [lifecycle_activity, Trailblazer::Activity::Introspect.Nodes(lifecycle_activity, id: "catch-before-#{reject_id}").task], + ) + + initial_lane_positions = Trailblazer::Workflow::Collaboration::Synchronous.initial_lane_positions(lanes) # we need to do this manually here, as initial_lane_positions isn't part of the {Schema.build} process. + extended_initial_lane_positions = initial_lane_positions.merge( + approver_activity => approver_start_suspend + ) + extended_initial_lane_positions = Trailblazer::Workflow::Collaboration::Positions.new(extended_initial_lane_positions.collect { |activity, task| Trailblazer::Workflow::Collaboration::Position.new(activity, task) }) + + return approver_activity, extended_message_flow, extended_initial_lane_positions + end +end