diff --git a/.aspire/settings.json b/.aspire/settings.json new file mode 100644 index 0000000..231f885 --- /dev/null +++ b/.aspire/settings.json @@ -0,0 +1,3 @@ +{ + "appHostPath": "..\\src\\Werkr.AppHost\\Werkr.AppHost.csproj" +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 0aa16a3..a63fa8f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,395 +1,395 @@ -# top-most EditorConfig file -root = true - -# Don't use tabs for indentation. -[*] -indent_style = space -end_of_line = lf -# (Please don't specify an indent_size here; that has too many unintended consequences.) - -# Code files -[*.{cs,csx,vb,vbx}] -indent_size = 4 -insert_final_newline = true -charset = utf-8 - -# XML project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -indent_size = 4 -charset = utf-8 - -# XML config files -[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] -indent_size = 2 - -# JSON files -[*.json] -charset = utf-8 -indent_size = 2 - -# Powershell files -[*.ps1] -charset = utf-8 -indent_size = 4 - -# Shell script files -[*.sh] -charset = utf-8 -indent_size = 2 - -# Dotnet code style settings: -[*.{cs,vb}] - -# IDE0055: Fix formatting -dotnet_diagnostic.IDE0055.severity = warning - -# Sort using and Import directives with System.* appearing first -dotnet_sort_system_directives_first = true -dotnet_separate_import_directive_groups = false -# Avoid "this." and "Me." if not necessary -dotnet_style_qualification_for_field = false:silent -dotnet_style_qualification_for_property = false:silent -dotnet_style_qualification_for_method = false:silent -dotnet_style_qualification_for_event = false:silent - -# Use language keywords instead of framework type names for type references -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion - -# Suggest more modern language features when available -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion - -# Non-private static fields are PascalCase -dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields -dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style - -dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field -dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected -dotnet_naming_symbols.non_private_static_fields.required_modifiers = static - -dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case - -# Non-private readonly fields are PascalCase -dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields -dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_static_field_style - -dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field -dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected -dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly - -dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case - -# Constants are PascalCase -dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants -dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style - -dotnet_naming_symbols.constants.applicable_kinds = field, local -dotnet_naming_symbols.constants.required_modifiers = const - -dotnet_naming_style.constant_style.capitalization = pascal_case - -# Static fields are camelCase and start with s_ -dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion -dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields -dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style - -dotnet_naming_symbols.static_fields.applicable_kinds = field -dotnet_naming_symbols.static_fields.required_modifiers = static - -dotnet_naming_style.static_field_style.capitalization = camel_case -dotnet_naming_style.static_field_style.required_prefix = s_ - -# Instance fields are camelCase and start with _ -dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion -dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields -dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style - -dotnet_naming_symbols.instance_fields.applicable_kinds = field - -dotnet_naming_style.instance_field_style.capitalization = camel_case -dotnet_naming_style.instance_field_style.required_prefix = _ - -# Locals and parameters are camelCase -dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion -dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters -dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style - -dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local - -dotnet_naming_style.camel_case_style.capitalization = camel_case - -# Local functions are PascalCase -dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions -dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style - -dotnet_naming_symbols.local_functions.applicable_kinds = local_function - -dotnet_naming_style.local_function_style.capitalization = pascal_case - -# By default, name items with PascalCase -dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members -dotnet_naming_rule.members_should_be_pascal_case.style = non_private_static_field_style - -dotnet_naming_symbols.all_members.applicable_kinds = * - -dotnet_naming_style.pascal_case_style.capitalization = pascal_case - -# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}' -dotnet_diagnostic.RS2008.severity = none - -# IDE0035: Remove unreachable code -dotnet_diagnostic.IDE0035.severity = warning - -# IDE0036: Order modifiers -dotnet_diagnostic.IDE0036.severity = warning - -# IDE0043: Format string contains invalid placeholder -dotnet_diagnostic.IDE0043.severity = warning - -# IDE0044: Make field readonly -dotnet_diagnostic.IDE0044.severity = warning - -# RS0016: Only enable if API files are present -dotnet_public_api_analyzer.require_api_files = true -dotnet_style_readonly_field= true:silent -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -end_of_line = lf -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_simplified_interpolation = true:suggestion -dotnet_style_namespace_match_folder = true:suggestion -dotnet_diagnostic.CA1838.severity = suggestion -dotnet_diagnostic.CA1848.severity = suggestion -dotnet_diagnostic.CA1873.severity = suggestion -dotnet_diagnostic.CA5350.severity = error -dotnet_diagnostic.CA5351.severity = error -dotnet_diagnostic.CA5359.severity = warning -dotnet_diagnostic.CA5360.severity = warning -dotnet_diagnostic.CA5364.severity = error -dotnet_diagnostic.CA5365.severity = suggestion -dotnet_diagnostic.CA5384.severity = warning -dotnet_diagnostic.CA5385.severity = warning -dotnet_diagnostic.CA5397.severity = error -dotnet_diagnostic.CA2201.severity = warning -dotnet_diagnostic.CA2251.severity = suggestion -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent -dotnet_style_allow_multiple_blank_lines_experimental = true:silent -dotnet_style_allow_statement_immediately_after_block_experimental = true:silent -dotnet_code_quality_unused_parameters = all:suggestion -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent - -# CSharp code style settings: -[*.cs] -max_line_length = 120 - -# Newline settings -csharp_new_line_before_open_brace =false -csharp_new_line_before_else =false -csharp_new_line_before_catch =false -csharp_new_line_before_finally =false -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces =false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_switch_labels = true -csharp_indent_labels = flush_left - -# Prefer "var" everywhere -csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = false:suggestion -csharp_style_var_elsewhere = false:suggestion - -# Prefer method-like constructs to have a block body -csharp_style_expression_bodied_methods = true:none -csharp_style_expression_bodied_constructors = true:none -csharp_style_expression_bodied_operators = true:none - -# Prefer property-like constructs to have an expression-body -csharp_style_expression_bodied_properties = true:none -csharp_style_expression_bodied_indexers = true:none -csharp_style_expression_bodied_accessors = true:none - -# Suggest more modern language features when available -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_throw_expression = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = ignore -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = true -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = true -csharp_space_between_method_declaration_empty_parameter_list_parentheses = true -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = true -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false - -# Blocks are allowed -csharp_prefer_braces = true:silent -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = true -csharp_style_expression_bodied_lambdas= true:silent -csharp_using_directive_placement = outside_namespace:silent -csharp_prefer_simple_using_statement = true:suggestion -csharp_style_namespace_declarations = file_scoped:warning -csharp_style_expression_bodied_local_functions = true:silent -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:suggestion -csharp_style_unused_value_expression_statement_preference = discard_variable:silent -dotnet_diagnostic.CA1805.severity = warning -dotnet_diagnostic.CA1869.severity = warning -dotnet_diagnostic.CA1873.severity = warning -dotnet_diagnostic.CA2016.severity = warning -dotnet_diagnostic.IDE0004.severity = suggestion -dotnet_diagnostic.IDE0005.severity = suggestion -dotnet_diagnostic.IDE0008.severity = suggestion -dotnet_diagnostic.IDE0016.severity = suggestion -dotnet_diagnostic.IDE0017.severity = suggestion -dotnet_diagnostic.IDE0020.severity = suggestion -dotnet_diagnostic.IDE0019.severity = suggestion -dotnet_diagnostic.IDE0018.severity = suggestion -dotnet_diagnostic.IDE0022.severity = suggestion -dotnet_diagnostic.IDE0023.severity = suggestion -dotnet_diagnostic.IDE0024.severity = suggestion -dotnet_diagnostic.IDE0025.severity = suggestion -dotnet_diagnostic.IDE0026.severity = suggestion -dotnet_diagnostic.IDE0027.severity = suggestion -dotnet_diagnostic.IDE0028.severity = suggestion -dotnet_diagnostic.IDE0029.severity = suggestion -dotnet_diagnostic.IDE0030.severity = suggestion -dotnet_diagnostic.IDE0031.severity = suggestion -dotnet_diagnostic.IDE0032.severity = suggestion -dotnet_diagnostic.IDE0034.severity = suggestion -dotnet_diagnostic.IDE0040.severity = suggestion -dotnet_diagnostic.IDE0041.severity = warning -dotnet_diagnostic.IDE0045.severity = suggestion -dotnet_diagnostic.IDE0046.severity = suggestion -dotnet_diagnostic.IDE0048.severity = suggestion -dotnet_diagnostic.IDE0054.severity = suggestion -dotnet_diagnostic.IDE0057.severity = suggestion -dotnet_diagnostic.IDE0056.severity = suggestion -dotnet_diagnostic.IDE0058.severity = warning -dotnet_diagnostic.IDE0059.severity = suggestion -dotnet_diagnostic.IDE0060.severity = warning -dotnet_diagnostic.IDE0063.severity = suggestion -dotnet_diagnostic.IDE0066.severity = suggestion -dotnet_diagnostic.IDE0071.severity = suggestion -dotnet_diagnostic.IDE0072.severity = suggestion -dotnet_diagnostic.IDE0075.severity = suggestion -dotnet_diagnostic.IDE0074.severity = suggestion -dotnet_diagnostic.IDE0090.severity = warning -dotnet_diagnostic.IDE0082.severity = suggestion -dotnet_diagnostic.IDE0083.severity = suggestion -dotnet_diagnostic.IDE0120.severity = suggestion -dotnet_diagnostic.IDE0270.severity = warning -dotnet_diagnostic.IDE0305.severity = warning -dotnet_diagnostic.IDE0330.severity = warning -dotnet_diagnostic.IDE2004.severity = suggestion -dotnet_diagnostic.IDE2002.severity = suggestion -dotnet_diagnostic.IDE2001.severity = suggestion -dotnet_diagnostic.IDE1006.severity = warning -dotnet_diagnostic.IDE0180.severity = suggestion -dotnet_diagnostic.MSTEST0037.severity = warning -dotnet_diagnostic.MSTEST0049.severity = warning -dotnet_diagnostic.MSTEST0058.severity = warning -dotnet_diagnostic.SYSLIB1045.severity = warning -csharp_prefer_static_local_function = true:suggestion -csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent -csharp_style_prefer_switch_expression = true:suggestion -csharp_style_prefer_pattern_matching = true:silent -csharp_style_prefer_not_pattern = true:suggestion -csharp_style_prefer_extended_property_pattern = true:suggestion -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = false:silent -csharp_style_prefer_utf8_string_literals = true:suggestion - -[src/CodeStyle/**.{cs,vb}] -# warning RS0005: Do not use generic CodeAction.Create to create CodeAction -dotnet_diagnostic.RS0005.severity = none - -[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures, VisualStudio}/**/*.{cs,vb}] - -# IDE0011: Add braces -csharp_prefer_braces = when_multiline:warning -# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 -dotnet_diagnostic.IDE0011.severity = warning - -# IDE0040: Add accessibility modifiers -dotnet_diagnostic.IDE0040.severity = warning - -# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings? -# IDE0051: Remove unused private member -dotnet_diagnostic.IDE0051.severity = warning - -# IDE0052: Remove unread private member -dotnet_diagnostic.IDE0052.severity = warning - -# IDE0059: Unnecessary assignment to a value -dotnet_diagnostic.IDE0059.severity = warning - -# IDE0060: Remove unused parameter -dotnet_diagnostic.IDE0060.severity = warning - -# CA1822: Make member static -dotnet_diagnostic.CA1822.severity = warning - -# Prefer "var" everywhere -dotnet_diagnostic.IDE0007.severity = warning -csharp_style_var_for_built_in_types = true:warning -csharp_style_var_when_type_is_apparent = true:warning -csharp_style_var_elsewhere = true:warning - -[src/{VisualStudio}/**/*.{cs,vb}] -# CA1822: Make member static -# Not enforced as a build 'warning' for 'VisualStudio' layer due to large number of false positives from https://github.com/dotnet/roslyn-analyzers/issues/3857 and https://github.com/dotnet/roslyn-analyzers/issues/3858 -# Additionally, there is a risk of accidentally breaking an internal API that partners rely on though IVT. -dotnet_diagnostic.CA1822.severity = suggestion +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +end_of_line = lf +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8 + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 4 +charset = utf-8 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +charset = utf-8 +indent_size = 2 + +# Powershell files +[*.ps1] +charset = utf-8 +indent_size = 4 + +# Shell script files +[*.sh] +charset = utf-8 +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] + +# IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity = warning + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Constants are PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static fields are camelCase and start with s_ +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_field_style.capitalization = camel_case +dotnet_naming_style.static_field_style.required_prefix = s_ + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}' +dotnet_diagnostic.RS2008.severity = none + +# IDE0035: Remove unreachable code +dotnet_diagnostic.IDE0035.severity = warning + +# IDE0036: Order modifiers +dotnet_diagnostic.IDE0036.severity = warning + +# IDE0043: Format string contains invalid placeholder +dotnet_diagnostic.IDE0043.severity = warning + +# IDE0044: Make field readonly +dotnet_diagnostic.IDE0044.severity = warning + +# RS0016: Only enable if API files are present +dotnet_public_api_analyzer.require_api_files = true +dotnet_style_readonly_field= true:silent +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +end_of_line = lf +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_diagnostic.CA1838.severity = suggestion +dotnet_diagnostic.CA1848.severity = suggestion +dotnet_diagnostic.CA1873.severity = suggestion +dotnet_diagnostic.CA5350.severity = error +dotnet_diagnostic.CA5351.severity = error +dotnet_diagnostic.CA5359.severity = warning +dotnet_diagnostic.CA5360.severity = warning +dotnet_diagnostic.CA5364.severity = error +dotnet_diagnostic.CA5365.severity = suggestion +dotnet_diagnostic.CA5384.severity = warning +dotnet_diagnostic.CA5385.severity = warning +dotnet_diagnostic.CA5397.severity = error +dotnet_diagnostic.CA2201.severity = warning +dotnet_diagnostic.CA2251.severity = suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent + +# CSharp code style settings: +[*.cs] +max_line_length = 120 + +# Newline settings +csharp_new_line_before_open_brace =false +csharp_new_line_before_else =false +csharp_new_line_before_catch =false +csharp_new_line_before_finally =false +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces =false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:suggestion +csharp_style_var_elsewhere = false:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = true:none +csharp_style_expression_bodied_constructors = true:none +csharp_style_expression_bodied_operators = true:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = true +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = true +csharp_space_between_method_declaration_empty_parameter_list_parentheses = true +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = true +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Blocks are allowed +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true +csharp_style_expression_bodied_lambdas= true:silent +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_expression_bodied_local_functions = true:silent +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +dotnet_diagnostic.CA1805.severity = warning +dotnet_diagnostic.CA1869.severity = warning +dotnet_diagnostic.CA1873.severity = warning +dotnet_diagnostic.CA2016.severity = warning +dotnet_diagnostic.IDE0004.severity = suggestion +dotnet_diagnostic.IDE0005.severity = suggestion +dotnet_diagnostic.IDE0008.severity = suggestion +dotnet_diagnostic.IDE0016.severity = suggestion +dotnet_diagnostic.IDE0017.severity = suggestion +dotnet_diagnostic.IDE0020.severity = suggestion +dotnet_diagnostic.IDE0019.severity = suggestion +dotnet_diagnostic.IDE0018.severity = suggestion +dotnet_diagnostic.IDE0022.severity = suggestion +dotnet_diagnostic.IDE0023.severity = suggestion +dotnet_diagnostic.IDE0024.severity = suggestion +dotnet_diagnostic.IDE0025.severity = suggestion +dotnet_diagnostic.IDE0026.severity = suggestion +dotnet_diagnostic.IDE0027.severity = suggestion +dotnet_diagnostic.IDE0028.severity = suggestion +dotnet_diagnostic.IDE0029.severity = suggestion +dotnet_diagnostic.IDE0030.severity = suggestion +dotnet_diagnostic.IDE0031.severity = suggestion +dotnet_diagnostic.IDE0032.severity = suggestion +dotnet_diagnostic.IDE0034.severity = suggestion +dotnet_diagnostic.IDE0040.severity = suggestion +dotnet_diagnostic.IDE0041.severity = warning +dotnet_diagnostic.IDE0045.severity = suggestion +dotnet_diagnostic.IDE0046.severity = suggestion +dotnet_diagnostic.IDE0048.severity = suggestion +dotnet_diagnostic.IDE0054.severity = suggestion +dotnet_diagnostic.IDE0057.severity = suggestion +dotnet_diagnostic.IDE0056.severity = suggestion +dotnet_diagnostic.IDE0058.severity = warning +dotnet_diagnostic.IDE0059.severity = suggestion +dotnet_diagnostic.IDE0060.severity = warning +dotnet_diagnostic.IDE0063.severity = suggestion +dotnet_diagnostic.IDE0066.severity = suggestion +dotnet_diagnostic.IDE0071.severity = suggestion +dotnet_diagnostic.IDE0072.severity = suggestion +dotnet_diagnostic.IDE0075.severity = suggestion +dotnet_diagnostic.IDE0074.severity = suggestion +dotnet_diagnostic.IDE0090.severity = warning +dotnet_diagnostic.IDE0082.severity = suggestion +dotnet_diagnostic.IDE0083.severity = suggestion +dotnet_diagnostic.IDE0120.severity = suggestion +dotnet_diagnostic.IDE0270.severity = warning +dotnet_diagnostic.IDE0305.severity = warning +dotnet_diagnostic.IDE0330.severity = warning +dotnet_diagnostic.IDE2004.severity = suggestion +dotnet_diagnostic.IDE2002.severity = suggestion +dotnet_diagnostic.IDE2001.severity = suggestion +dotnet_diagnostic.IDE1006.severity = warning +dotnet_diagnostic.IDE0180.severity = suggestion +dotnet_diagnostic.MSTEST0037.severity = warning +dotnet_diagnostic.MSTEST0049.severity = warning +dotnet_diagnostic.MSTEST0058.severity = warning +dotnet_diagnostic.SYSLIB1045.severity = warning +csharp_prefer_static_local_function = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = false:silent +csharp_style_prefer_utf8_string_literals = true:suggestion + +[src/CodeStyle/**.{cs,vb}] +# warning RS0005: Do not use generic CodeAction.Create to create CodeAction +dotnet_diagnostic.RS0005.severity = none + +[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures, VisualStudio}/**/*.{cs,vb}] + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline:warning +# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 +dotnet_diagnostic.IDE0011.severity = warning + +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.IDE0040.severity = warning + +# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings? +# IDE0051: Remove unused private member +dotnet_diagnostic.IDE0051.severity = warning + +# IDE0052: Remove unread private member +dotnet_diagnostic.IDE0052.severity = warning + +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.IDE0059.severity = warning + +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = warning + +# CA1822: Make member static +dotnet_diagnostic.CA1822.severity = warning + +# Prefer "var" everywhere +dotnet_diagnostic.IDE0007.severity = warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:warning + +[src/{VisualStudio}/**/*.{cs,vb}] +# CA1822: Make member static +# Not enforced as a build 'warning' for 'VisualStudio' layer due to large number of false positives from https://github.com/dotnet/roslyn-analyzers/issues/3857 and https://github.com/dotnet/roslyn-analyzers/issues/3858 +# Additionally, there is a risk of accidentally breaking an internal API that partners rely on though IVT. +dotnet_diagnostic.CA1822.severity = suggestion diff --git a/.github/workflows/DocFX_gh-pages.yml b/.github/workflows/DocFX_gh-pages.yml index 0c3744a..c61978b 100644 --- a/.github/workflows/DocFX_gh-pages.yml +++ b/.github/workflows/DocFX_gh-pages.yml @@ -3,116 +3,57 @@ on: push: branches: - main + paths: + - 'docs/**' + - 'src/**/*.csproj' + - '.github/workflows/DocFX_gh-pages.yml' + +permissions: + contents: write + jobs: - document: - runs-on: windows-latest - env: - DOTNET_NOLOGO: true - DOCFX_SOURCE_BRANCH_NAME: ${{ github.ref }} - strategy: - matrix: - dotnet-version: [ '7.0.x' ] - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - name: Check out Werkr.Common - uses: actions/checkout@v2 - with: - repository: DarkgreyDevelopment/Werkr.Common - path: src/Werkr.Common - token: ${{ secrets.CI_TOKEN }} - - name: Check out Werkr.Common.Configuration - uses: actions/checkout@v2 - with: - repository: DarkgreyDevelopment/Werkr.Common.Configuration - path: src/Werkr.Common.Configuration - token: ${{ secrets.CI_TOKEN }} - - name: Check out Werkr.Installers - uses: actions/checkout@v2 - with: - repository: DarkgreyDevelopment/Werkr.Installers - path: src/Werkr.Installers - token: ${{ secrets.CI_TOKEN }} - - name: Check out Werkr.Server - uses: actions/checkout@v2 - with: - repository: DarkgreyDevelopment/Werkr.Server - path: src/Werkr.Server - token: ${{ secrets.CI_TOKEN }} - - name: Check out Werkr.Agent - uses: actions/checkout@v2 - with: - repository: DarkgreyDevelopment/Werkr.Agent - path: src/Werkr.Agent - token: ${{ secrets.CI_TOKEN }} - - name: Get DocFX - shell: pwsh - run: | - $IWRParams = @{ - Uri = "https://github.com/dotnet/docfx/releases/download/v2.59.4/docfx.zip" - OutFile = '${{ github.workspace }}/docfx.zip' - Method = 'Get' - } - Invoke-WebRequest @IWRParams - Expand-Archive -Path '${{ github.workspace }}/docfx.zip' -DestinationPath '${{ github.workspace }}/docfx' - - name: Custom File processing. - shell: pwsh - run: | - $DocsPath = '${{ github.workspace }}/docs' - $CopyParams = @{ - Verbose = $true - Force = $true - } - copy-item -Path '${{ github.workspace }}/LICENSE' -Destination "$DocsPath/LICENSE.md" @CopyParams - copy-item -Path '${{ github.workspace }}/README.md' -Destination "$DocsPath/index.md" @CopyParams - copy-Item -Path '${{ github.workspace }}/docs/docfx/*' -Destination $DocsPath -Exclude README.md -Verbose -Recurse - - name: Generate Documentation and build site. - shell: pwsh - run: | - Write-Host "`nGenerating API documentation:" - & '${{ github.workspace }}/docfx/docfx.exe' metadata '${{ github.workspace }}/docs/docfx.json' - Write-Host "`nCreating docfx site:" - & '${{ github.workspace }}/docfx/docfx.exe' '${{ github.workspace }}/docs/docfx.json' - - name: Compress Site for upload as Artifact. - shell: pwsh - run: | - $CopyToSiteParams = @{ - Destination = '${{ github.workspace }}/docs/_site' - Verbose = $true - } - copy-item -Path '${{ github.workspace }}/docs/CNAME' @CopyToSiteParams - copy-item -Path '${{ github.workspace }}/docs/_config.yml' @CopyToSiteParams - Write-Host "`nCompressing Site for Artifact Upload" - Compress-Archive -Path '${{ github.workspace }}/docs/_site' -DestinationPath '${{ github.workspace }}/docs/_site.zip' - - name: Upload Artifacts - uses: actions/upload-artifact@v1 - with: - name: site - path: ${{ github.workspace }}/docs/_site.zip - publish: - needs: document + build-and-publish: runs-on: ubuntu-latest env: DOTNET_NOLOGO: true - DOCFX_SOURCE_BRANCH_NAME: ${{ github.ref }} - strategy: - matrix: - dotnet-version: [ '7.0.x' ] + DOTNET_CLI_TELEMETRY_OPTOUT: true steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Download Artifacts - uses: actions/download-artifact@v1 - with: - name: site - path: ${{ github.workspace }}/download - - name: Verify WorkSpace Contents - shell: pwsh + - name: Check out repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Install DocFX + run: dotnet tool install -g docfx --version 2.78.3 + + - name: Prepare documentation sources + run: | + cp LICENSE docs/LICENSE.md + cp README.md docs/index.md + cp -r docs/docfx/* docs/ 2>/dev/null || true + + - name: Generate API metadata + run: docfx metadata docs/docfx.json + + - name: Build documentation site + run: docfx docs/docfx.json + + - name: Copy CNAME and config run: | - Write-Host "`Extracting Site." - Expand-Archive -Path '${{ github.workspace }}/download/_site.zip' -DestinationPath '${{ github.workspace }}' - - name: Publish Site Content + cp docs/CNAME docs/_site/ 2>/dev/null || true + cp docs/_config.yml docs/_site/ 2>/dev/null || true + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: docfx-site + path: docs/_site + + - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@v4 with: - BRANCH: gh-pages - FOLDER: ${{ github.workspace }}/_site + branch: gh-pages + folder: docs/_site diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9e6789..b56ebb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,79 +1,98 @@ -name: CI - -on: - pull_request: - branches: [main, develop] - push: - branches: [main, develop] - -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-and-test: - name: Build & Test - runs-on: ubuntu-latest - env: - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Restore tools - run: dotnet tool restore - - - name: Determine version - id: version - run: | - VERSION_JSON=$(dotnet gitversion /output json) - echo "semVer=$(echo $VERSION_JSON | jq -r '.SemVer')" >> $GITHUB_OUTPUT - echo "assemblySemVer=$(echo $VERSION_JSON | jq -r '.AssemblySemVer')" >> $GITHUB_OUTPUT - echo "assemblySemFileVer=$(echo $VERSION_JSON | jq -r '.AssemblySemFileVer')" >> $GITHUB_OUTPUT - echo "informationalVersion=$(echo $VERSION_JSON | jq -r '.InformationalVersion')" >> $GITHUB_OUTPUT - - - name: Restore dependencies - run: | - # Backup lock files before restore - find . -name "packages.lock.json" -exec cp {} {}.backup \; - - # Restore with force-evaluate for linux-x64 - dotnet restore Werkr.slnx --force-evaluate - - # Validate lock file changes (skip Windows-only Installer projects) - for lockfile in $(find . -name "packages.lock.json" ! -name "*.backup" ! -path "*/Installer/*"); do - backup="${lockfile}.backup" - if [ -f "$backup" ]; then - pwsh scripts/Test-LockFileChanges.ps1 -BackupPath "$backup" -CurrentPath "$lockfile" -ToPlatform "linux-x64" - rm -f "$backup" - fi - done - - - name: Build - run: > - dotnet build Werkr.slnx -c Release --no-restore - /p:Version=${{ steps.version.outputs.semVer }} - /p:AssemblyVersion=${{ steps.version.outputs.assemblySemVer }} - /p:FileVersion=${{ steps.version.outputs.assemblySemFileVer }} - /p:InformationalVersion="${{ steps.version.outputs.informationalVersion }}" - - - name: Test - run: > - dotnet test --solution Werkr.slnx -c Release --no-build - --logger "trx;LogFileName=results.trx" - --results-directory TestResults - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: TestResults/**/*.trx +name: CI + +on: + pull_request: + branches: [main, develop] + push: + branches: [main, develop] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build & Test + runs-on: ubuntu-latest + env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore tools + run: dotnet tool restore + + - name: Determine version + id: version + run: | + VERSION_JSON=$(dotnet gitversion /output json) + echo "semVer=$(echo $VERSION_JSON | jq -r '.SemVer')" >> $GITHUB_OUTPUT + echo "assemblySemVer=$(echo $VERSION_JSON | jq -r '.AssemblySemVer')" >> $GITHUB_OUTPUT + echo "assemblySemFileVer=$(echo $VERSION_JSON | jq -r '.AssemblySemFileVer')" >> $GITHUB_OUTPUT + echo "informationalVersion=$(echo $VERSION_JSON | jq -r '.InformationalVersion')" >> $GITHUB_OUTPUT + + - name: Setup Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: src/Werkr.Server/graph-ui/package-lock.json + + - name: Install graph-ui dependencies + run: npm ci --prefix src/Werkr.Server/graph-ui + + - name: Run JS tests + run: npm test --prefix src/Werkr.Server/graph-ui + + - name: Build JS bundles (production) + run: npm run build:prod --prefix src/Werkr.Server/graph-ui + + - name: Check bundle sizes + run: node src/Werkr.Server/graph-ui/scripts/check-bundle-size.mjs + + - name: Restore dependencies + run: | + # Backup lock files before restore + find . -name "packages.lock.json" -exec cp {} {}.backup \; + + # Restore with force-evaluate for linux-x64 + dotnet restore Werkr.slnx --force-evaluate + + # Validate lock file changes (skip Windows-only Installer projects) + for lockfile in $(find . -name "packages.lock.json" ! -name "*.backup" ! -path "*/Installer/*"); do + backup="${lockfile}.backup" + if [ -f "$backup" ]; then + pwsh scripts/Test-LockFileChanges.ps1 -BackupPath "$backup" -CurrentPath "$lockfile" -ToPlatform "linux-x64" + rm -f "$backup" + fi + done + + - name: Build + run: > + dotnet build Werkr.slnx -c Release --no-restore + /p:Version=${{ steps.version.outputs.semVer }} + /p:AssemblyVersion=${{ steps.version.outputs.assemblySemVer }} + /p:FileVersion=${{ steps.version.outputs.assemblySemFileVer }} + /p:InformationalVersion="${{ steps.version.outputs.informationalVersion }}" + + - name: Test + run: > + dotnet test --solution Werkr.slnx -c Release --no-build + --logger "trx;LogFileName=results.trx" + --results-directory TestResults + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: TestResults/**/*.trx diff --git a/.gitignore b/.gitignore index a986f85..eb4a183 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ docs/projects/* # Visual Studio Code Configuration .vscode/* +!.vscode/tasks.json # Visual Studio Configuration .vs/* @@ -78,6 +79,10 @@ docs/projects/* # SQLite Database Files *.db + +# Node / TypeScript build artifacts +node_modules/ +**/wwwroot/js/dist/ *.db-shm *.db-wal @@ -94,6 +99,9 @@ docker-compose.override.yml # Aspire publish output aspire-output/ +# User-specific project files +*.user + # Secrets directory secrets/ @@ -132,4 +140,4 @@ logs/ *.dll *.exe -*.pdb \ No newline at end of file +*.pdb diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..4b7af21 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,447 @@ +{ + "version": "2.0.0", + "inputs": [ + { + "id": "migrationName", + "type": "promptString", + "description": "Name for the EF Core migration", + "default": "migration" + } + ], + "tasks": [ + { + "label": "verify:restore", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "restore", + "Werkr.slnx" + ], + "group": "build", + "problemMatcher": "$msCompile", + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "showReuseMessage": false + } + }, + { + "label": "verify:build", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "build", + "Werkr.slnx" + ], + "group": "build", + "problemMatcher": "$msCompile", + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "showReuseMessage": false + } + }, + { + "label": "verify:format", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "format", + "Werkr.slnx" + ], + "problemMatcher": [], + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "showReuseMessage": false + } + }, + { + "label": "verify:test-unit", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "test", + "--project", + "src/Test/Werkr.Tests.Data/Werkr.Tests.Data.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:test-integration", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "test", + "--project", + "src/Test/Werkr.Tests.Server/Werkr.Tests.Server.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:test-e2e", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "test", + "--project", + "src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:start-apphost", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "run", + "--project", + "src/Werkr.AppHost/Werkr.AppHost.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:docker-check", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "docker", + "compose", + "config" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:docker-build", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-File", + "${workspaceFolder}/scripts/docker-build.ps1" + ], + "group": "build", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "docker:start", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "docker", + "compose", + "up", + "-d" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "docker:stop", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "docker", + "compose", + "down" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "docker:restart", + "dependsOn": [ + "docker:stop", + "docker:start" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:app:postgres", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data", + "--startup-project", + "src/Werkr.Api", + "--context", + "PostgresWerkrDbContext", + "--output-dir", + "Migrations/Postgres" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:app:sqlite", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data", + "--startup-project", + "src/Werkr.Api", + "--context", + "SqliteWerkrDbContext", + "--output-dir", + "Migrations/Sqlite" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:app:both", + "dependsOn": [ + "ef:app:postgres", + "ef:app:sqlite" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:identity:postgres", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data.Identity", + "--startup-project", + "src/Werkr.Server", + "--context", + "PostgresWerkrIdentityDbContext", + "--output-dir", + "Migrations/Postgres" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:identity:sqlite", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data.Identity", + "--startup-project", + "src/Werkr.Server", + "--context", + "SqliteWerkrIdentityDbContext", + "--output-dir", + "Migrations/Sqlite" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:identity:both", + "dependsOn": [ + "ef:identity:postgres", + "ef:identity:sqlite" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:all", + "dependsOn": [ + "ef:app:both", + "ef:identity:both" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:build-server", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet build src/Werkr.Server/Werkr.Server.csproj'" + }, + { + "label": "verify:test-e2e-verbose", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --verbosity normal 2>&1 | tail -50'" + }, + { + "label": "verify:test-e2e-failures", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj -- --report-trx 2>&1 | grep -i -E \"failed|error|FAIL\" | head -20'" + }, + { + "label": "verify:e2e-fail-detail", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --no-build 2>&1 | grep -i -E \"failed|FAIL\" | head -20'" + }, + { + "label": "verify:e2e-tail", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj'" + }, + { + "label": "verify:test-server", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Server/Werkr.Tests.Server.csproj'" + } + ] +} diff --git a/Directory.Packages.props b/Directory.Packages.props index b6542c9..a26b719 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,7 @@ true + true @@ -14,35 +15,35 @@ - - - - + + + + - + - + - + - - - - - + + + + + - - - - + + + + @@ -53,7 +54,7 @@ - + @@ -67,8 +68,8 @@ - - - + + + - \ No newline at end of file + diff --git a/scripts/Test-LockFileChanges.ps1 b/scripts/Test-LockFileChanges.ps1 index 82197e3..25c94a8 100644 --- a/scripts/Test-LockFileChanges.ps1 +++ b/scripts/Test-LockFileChanges.ps1 @@ -1,304 +1,304 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Verifies that only platform-specific packages changed in a NuGet lock file. - -.DESCRIPTION - Compares a backup lock file against the current lock file and validates that - only expected platform-specific Aspire packages differ between them. - - Expected platform-specific packages: - - Aspire.Dashboard.Sdk. - - Aspire.Hosting.Orchestration. - -.PARAMETER BackupPath - Path to the backup lock file (before regeneration). - -.PARAMETER CurrentPath - Path to the current lock file (after regeneration). - -.PARAMETER FromPlatform - The source platform RID (e.g., linux-x64, linux-arm64). Defaults to linux-x64. - -.PARAMETER ToPlatform - The target platform RID (e.g., linux-x64, linux-arm64). Defaults to linux-arm64. - -.EXAMPLE - ./Test-LockFileChanges.ps1 -BackupPath packages.lock.json.backup -CurrentPath packages.lock.json - -.EXAMPLE - ./Test-LockFileChanges.ps1 -BackupPath packages.lock.json.backup -CurrentPath packages.lock.json -FromPlatform linux-arm64 -ToPlatform linux-x64 -#> -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [string]$BackupPath, - - [Parameter(Mandatory)] - [string]$CurrentPath, - - [Parameter()] - [ValidatePattern('^((linux|win)-(x64|arm64)|osx-arm64)$')] - [string]$FromPlatform = 'linux-x64', - - [Parameter()] - [ValidatePattern('^((linux|win)-(x64|arm64)|osx-arm64)$')] - [string]$ToPlatform = 'linux-arm64' -) - -$ErrorActionPreference = 'Stop' - -# Validate platform format -Write-Host "Platform transition: $FromPlatform -> $ToPlatform" - -# Platform-specific package patterns that are allowed to differ -# These packages have platform-specific variants that will change when switching RIDs -# We allow any valid platform suffix since we support multiple target platforms -# Microsoft.NET.ILLink.Tasks is implicitly added by .NET 10 SDK when PublishSingleFile+SelfContained are enabled -$AllowedPackagePatterns = @( - '^Aspire\.Dashboard\.Sdk\.(linux|win|osx)-(x64|arm64)$', - '^Aspire\.Hosting\.Orchestration\.(linux|win|osx)-(x64|arm64)$', - '^Microsoft\.NET\.ILLink\.Tasks$' -) - -function Test-AllowedPackage { - param([string]$PackageName) - foreach ($pattern in $AllowedPackagePatterns) { - if ($PackageName -match $pattern) { - return $true - } - } - return $false -} - -function Test-ProjectDependencyChangesAllowed { - param( - [hashtable]$BackupEntry, - [hashtable]$CurrentEntry - ) - - if (($null -eq $BackupEntry) -or ($null -eq $CurrentEntry)) { - return $false - } - - if (($BackupEntry.type -ne 'Project') -or ($CurrentEntry.type -ne 'Project')) { - return $false - } - - $backupDeps = @{} - $currentDeps = @{} - - if ($BackupEntry.ContainsKey('dependencies') -and ($null -ne $BackupEntry.dependencies)) { - $backupDeps = $BackupEntry.dependencies - } - if ($CurrentEntry.ContainsKey('dependencies') -and ($null -ne $CurrentEntry.dependencies)) { - $currentDeps = $CurrentEntry.dependencies - } - - # True means: the only differences between the two Project dependency maps are - # allowed platform-specific packages, and there is at least one such difference. - $allDependencyNames = @($backupDeps.Keys; $currentDeps.Keys) | Select-Object -Unique - $foundAllowedDifference = $false - - foreach ($dependencyName in $allDependencyNames) { - $inBackup = $backupDeps.ContainsKey($dependencyName) - $inCurrent = $currentDeps.ContainsKey($dependencyName) - - if (($inBackup -and $inCurrent) -and ($backupDeps[$dependencyName] -eq $currentDeps[$dependencyName])) { - continue - } - - if (-not (Test-AllowedPackage $dependencyName)) { - return $false - } - - $foundAllowedDifference = $true - } - - return $foundAllowedDifference -} - -function Test-LockEntryDifferent { - param( - [hashtable]$BackupEntry, - [hashtable]$CurrentEntry - ) - - if (($null -eq $BackupEntry) -or ($null -eq $CurrentEntry)) { - return $true - } - - if (($BackupEntry.type -eq 'Transitive') -and ($CurrentEntry.type -eq 'Transitive')) { - return $BackupEntry.contentHash -ne $CurrentEntry.contentHash - } - - if (($BackupEntry.type -eq 'Project') -and ($CurrentEntry.type -eq 'Project')) { - $backupDeps = @{} - $currentDeps = @{} - - if ($BackupEntry.ContainsKey('dependencies') -and ($null -ne $BackupEntry.dependencies)) { - $backupDeps = $BackupEntry.dependencies - } - if ($CurrentEntry.ContainsKey('dependencies') -and ($null -ne $CurrentEntry.dependencies)) { - $currentDeps = $CurrentEntry.dependencies - } - - $allDependencyNames = @($backupDeps.Keys; $currentDeps.Keys) | Select-Object -Unique - foreach ($dependencyName in $allDependencyNames) { - $inBackup = $backupDeps.ContainsKey($dependencyName) - $inCurrent = $currentDeps.ContainsKey($dependencyName) - - if ( - ($inBackup -and $inCurrent) -and - ($backupDeps[$dependencyName] -eq $currentDeps[$dependencyName]) - ) { - continue - } - - return $true - } - - return $false - } - - $backupJson = $BackupEntry | ConvertTo-Json -Compress - $currentJson = $CurrentEntry | ConvertTo-Json -Compress - return $backupJson -ne $currentJson -} - -# Read and parse both lock files -$backup = Get-Content $BackupPath -Raw | ConvertFrom-Json -AsHashtable -$current = Get-Content $CurrentPath -Raw | ConvertFrom-Json -AsHashtable - -$unexpectedChanges = @() -$expectedChanges = @() - -function Test-PlatformFramework { - param([string]$Framework) - # Framework entries like "net10.0/linux-x64" or "net10.0/win-arm64" are platform-specific - return $Framework -match '^net\d+\.\d+/(linux|win|osx)-(x64|x86|arm64|arm)$' -} - -# Compare each target framework -foreach ($framework in $current.dependencies.Keys) { - $backupDeps = $backup.dependencies[$framework] - $currentDeps = $current.dependencies[$framework] - - if ($null -eq $backupDeps) { - if (-not (Test-PlatformFramework $framework)) { - $unexpectedChanges += "New framework added: $framework" - } - continue - } - - # Find all unique package names across both - $allPackages = @($backupDeps.Keys) + @($currentDeps.Keys) | Select-Object -Unique - - foreach ($package in $allPackages) { - $inBackup = $backupDeps.ContainsKey($package) - $inCurrent = $currentDeps.ContainsKey($package) - - if ($inBackup -and $inCurrent) { - # Package exists in both - check if it changed - $backupEntry = $backupDeps[$package] - $currentEntry = $currentDeps[$package] - - if (Test-LockEntryDifferent -BackupEntry $backupEntry -CurrentEntry $currentEntry) { - if (Test-AllowedPackage $package) { - $expectedChanges += [PSCustomObject]@{ - Package = $package - Type = 'Modified' - Framework = $framework - } - } elseif (Test-ProjectDependencyChangesAllowed -BackupEntry $backupEntry -CurrentEntry $currentEntry) { - $expectedChanges += [PSCustomObject]@{ - Package = $package - Type = 'Modified' - Framework = $framework - } - } else { - $unexpectedChanges += "Package modified: $package in $framework" - } - } - } elseif ($inBackup -and -not $inCurrent) { - # Package removed - if (Test-AllowedPackage $package) { - $expectedChanges += [PSCustomObject]@{ - Package = $package - Type = 'Removed' - Framework = $framework - } - } else { - $unexpectedChanges += "Package removed: $package from $framework" - } - } elseif (-not $inBackup -and $inCurrent) { - # Package added - if (Test-AllowedPackage $package) { - $expectedChanges += [PSCustomObject]@{ - Package = $package - Type = 'Added' - Framework = $framework - } - } else { - $unexpectedChanges += "Package added: $package to $framework" - } - } - } -} - -# Check for removed frameworks -foreach ($framework in $backup.dependencies.Keys) { - if (-not $current.dependencies.ContainsKey($framework)) { - if (-not (Test-PlatformFramework $framework)) { - $unexpectedChanges += "Framework removed: $framework" - } - } -} - -# Report results -if ($expectedChanges.Count -gt 0) { - Write-Host 'Platform-specific package changes:' - foreach ($change in $expectedChanges) { - $versionInfo = [string]::Empty - if ($change.Type -eq 'Modified') { - $backupEntry = $backup.dependencies[$change.Framework][$change.Package] - $currentEntry = $current.dependencies[$change.Framework][$change.Package] - if ($null -ne $backupEntry -and $null -ne $currentEntry -and $backupEntry.ContainsKey('resolved') -and $currentEntry.ContainsKey('resolved')) { - $backupVersion = $backupEntry.resolved - $currentVersion = $currentEntry.resolved - $versionInfo = " (version: $backupVersion -> $currentVersion)" - } - } elseif ($change.Type -eq 'Removed') { - $backupEntry = $backup.dependencies[$change.Framework][$change.Package] - if ($null -ne $backupEntry -and $backupEntry.ContainsKey('resolved')) { - $backupVersion = $backupEntry.resolved - $versionInfo = " (version: $backupVersion)" - } - } elseif ($change.Type -eq 'Added') { - $currentEntry = $current.dependencies[$change.Framework][$change.Package] - if ($null -ne $currentEntry -and $currentEntry.ContainsKey('resolved')) { - $currentVersion = $currentEntry.resolved - $versionInfo = " (version: $currentVersion)" - } - } - Write-Host " [$($change.Type)] $($change.Package)$versionInfo" - } - Write-Host [string]::Empty -} - -if ($unexpectedChanges.Count -gt 0) { - Write-Error ( - 'ERROR: Non-platform-specific packages changed in lock file! ' + - 'Only platform-specific Aspire packages (and the project entries that reference them) should differ between platforms.' + - "`nUnexpected changes detected:" + ($unexpectedChanges | ForEach-Object { "`n - $_" }) - ) - exit 1 -} - -if ($expectedChanges.Count -eq 0) { - Write-Host 'No changes detected in lock file' -} else { - Write-Host 'Lock file updated successfully - only platform-specific packages changed' -} - -exit 0 +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Verifies that only platform-specific packages changed in a NuGet lock file. + +.DESCRIPTION + Compares a backup lock file against the current lock file and validates that + only expected platform-specific Aspire packages differ between them. + + Expected platform-specific packages: + - Aspire.Dashboard.Sdk. + - Aspire.Hosting.Orchestration. + +.PARAMETER BackupPath + Path to the backup lock file (before regeneration). + +.PARAMETER CurrentPath + Path to the current lock file (after regeneration). + +.PARAMETER FromPlatform + The source platform RID (e.g., linux-x64, linux-arm64). Defaults to linux-x64. + +.PARAMETER ToPlatform + The target platform RID (e.g., linux-x64, linux-arm64). Defaults to linux-arm64. + +.EXAMPLE + ./Test-LockFileChanges.ps1 -BackupPath packages.lock.json.backup -CurrentPath packages.lock.json + +.EXAMPLE + ./Test-LockFileChanges.ps1 -BackupPath packages.lock.json.backup -CurrentPath packages.lock.json -FromPlatform linux-arm64 -ToPlatform linux-x64 +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$BackupPath, + + [Parameter(Mandatory)] + [string]$CurrentPath, + + [Parameter()] + [ValidatePattern('^((linux|win)-(x64|arm64)|osx-arm64)$')] + [string]$FromPlatform = 'linux-x64', + + [Parameter()] + [ValidatePattern('^((linux|win)-(x64|arm64)|osx-arm64)$')] + [string]$ToPlatform = 'linux-arm64' +) + +$ErrorActionPreference = 'Stop' + +# Validate platform format +Write-Host "Platform transition: $FromPlatform -> $ToPlatform" + +# Platform-specific package patterns that are allowed to differ +# These packages have platform-specific variants that will change when switching RIDs +# We allow any valid platform suffix since we support multiple target platforms +# Microsoft.NET.ILLink.Tasks is implicitly added by .NET 10 SDK when PublishSingleFile+SelfContained are enabled +$AllowedPackagePatterns = @( + '^Aspire\.Dashboard\.Sdk\.(linux|win|osx)-(x64|arm64)$', + '^Aspire\.Hosting\.Orchestration\.(linux|win|osx)-(x64|arm64)$', + '^Microsoft\.NET\.ILLink\.Tasks$' +) + +function Test-AllowedPackage { + param([string]$PackageName) + foreach ($pattern in $AllowedPackagePatterns) { + if ($PackageName -match $pattern) { + return $true + } + } + return $false +} + +function Test-ProjectDependencyChangesAllowed { + param( + [hashtable]$BackupEntry, + [hashtable]$CurrentEntry + ) + + if (($null -eq $BackupEntry) -or ($null -eq $CurrentEntry)) { + return $false + } + + if (($BackupEntry.type -ne 'Project') -or ($CurrentEntry.type -ne 'Project')) { + return $false + } + + $backupDeps = @{} + $currentDeps = @{} + + if ($BackupEntry.ContainsKey('dependencies') -and ($null -ne $BackupEntry.dependencies)) { + $backupDeps = $BackupEntry.dependencies + } + if ($CurrentEntry.ContainsKey('dependencies') -and ($null -ne $CurrentEntry.dependencies)) { + $currentDeps = $CurrentEntry.dependencies + } + + # True means: the only differences between the two Project dependency maps are + # allowed platform-specific packages, and there is at least one such difference. + $allDependencyNames = @($backupDeps.Keys; $currentDeps.Keys) | Select-Object -Unique + $foundAllowedDifference = $false + + foreach ($dependencyName in $allDependencyNames) { + $inBackup = $backupDeps.ContainsKey($dependencyName) + $inCurrent = $currentDeps.ContainsKey($dependencyName) + + if (($inBackup -and $inCurrent) -and ($backupDeps[$dependencyName] -eq $currentDeps[$dependencyName])) { + continue + } + + if (-not (Test-AllowedPackage $dependencyName)) { + return $false + } + + $foundAllowedDifference = $true + } + + return $foundAllowedDifference +} + +function Test-LockEntryDifferent { + param( + [hashtable]$BackupEntry, + [hashtable]$CurrentEntry + ) + + if (($null -eq $BackupEntry) -or ($null -eq $CurrentEntry)) { + return $true + } + + if (($BackupEntry.type -eq 'Transitive') -and ($CurrentEntry.type -eq 'Transitive')) { + return $BackupEntry.contentHash -ne $CurrentEntry.contentHash + } + + if (($BackupEntry.type -eq 'Project') -and ($CurrentEntry.type -eq 'Project')) { + $backupDeps = @{} + $currentDeps = @{} + + if ($BackupEntry.ContainsKey('dependencies') -and ($null -ne $BackupEntry.dependencies)) { + $backupDeps = $BackupEntry.dependencies + } + if ($CurrentEntry.ContainsKey('dependencies') -and ($null -ne $CurrentEntry.dependencies)) { + $currentDeps = $CurrentEntry.dependencies + } + + $allDependencyNames = @($backupDeps.Keys; $currentDeps.Keys) | Select-Object -Unique + foreach ($dependencyName in $allDependencyNames) { + $inBackup = $backupDeps.ContainsKey($dependencyName) + $inCurrent = $currentDeps.ContainsKey($dependencyName) + + if ( + ($inBackup -and $inCurrent) -and + ($backupDeps[$dependencyName] -eq $currentDeps[$dependencyName]) + ) { + continue + } + + return $true + } + + return $false + } + + $backupJson = $BackupEntry | ConvertTo-Json -Compress + $currentJson = $CurrentEntry | ConvertTo-Json -Compress + return $backupJson -ne $currentJson +} + +# Read and parse both lock files +$backup = Get-Content $BackupPath -Raw | ConvertFrom-Json -AsHashtable +$current = Get-Content $CurrentPath -Raw | ConvertFrom-Json -AsHashtable + +$unexpectedChanges = @() +$expectedChanges = @() + +function Test-PlatformFramework { + param([string]$Framework) + # Framework entries like "net10.0/linux-x64" or "net10.0/win-arm64" are platform-specific + return $Framework -match '^net\d+\.\d+/(linux|win|osx)-(x64|x86|arm64|arm)$' +} + +# Compare each target framework +foreach ($framework in $current.dependencies.Keys) { + $backupDeps = $backup.dependencies[$framework] + $currentDeps = $current.dependencies[$framework] + + if ($null -eq $backupDeps) { + if (-not (Test-PlatformFramework $framework)) { + $unexpectedChanges += "New framework added: $framework" + } + continue + } + + # Find all unique package names across both + $allPackages = @($backupDeps.Keys) + @($currentDeps.Keys) | Select-Object -Unique + + foreach ($package in $allPackages) { + $inBackup = $backupDeps.ContainsKey($package) + $inCurrent = $currentDeps.ContainsKey($package) + + if ($inBackup -and $inCurrent) { + # Package exists in both - check if it changed + $backupEntry = $backupDeps[$package] + $currentEntry = $currentDeps[$package] + + if (Test-LockEntryDifferent -BackupEntry $backupEntry -CurrentEntry $currentEntry) { + if (Test-AllowedPackage $package) { + $expectedChanges += [PSCustomObject]@{ + Package = $package + Type = 'Modified' + Framework = $framework + } + } elseif (Test-ProjectDependencyChangesAllowed -BackupEntry $backupEntry -CurrentEntry $currentEntry) { + $expectedChanges += [PSCustomObject]@{ + Package = $package + Type = 'Modified' + Framework = $framework + } + } else { + $unexpectedChanges += "Package modified: $package in $framework" + } + } + } elseif ($inBackup -and -not $inCurrent) { + # Package removed + if (Test-AllowedPackage $package) { + $expectedChanges += [PSCustomObject]@{ + Package = $package + Type = 'Removed' + Framework = $framework + } + } else { + $unexpectedChanges += "Package removed: $package from $framework" + } + } elseif (-not $inBackup -and $inCurrent) { + # Package added + if (Test-AllowedPackage $package) { + $expectedChanges += [PSCustomObject]@{ + Package = $package + Type = 'Added' + Framework = $framework + } + } else { + $unexpectedChanges += "Package added: $package to $framework" + } + } + } +} + +# Check for removed frameworks +foreach ($framework in $backup.dependencies.Keys) { + if (-not $current.dependencies.ContainsKey($framework)) { + if (-not (Test-PlatformFramework $framework)) { + $unexpectedChanges += "Framework removed: $framework" + } + } +} + +# Report results +if ($expectedChanges.Count -gt 0) { + Write-Host 'Platform-specific package changes:' + foreach ($change in $expectedChanges) { + $versionInfo = [string]::Empty + if ($change.Type -eq 'Modified') { + $backupEntry = $backup.dependencies[$change.Framework][$change.Package] + $currentEntry = $current.dependencies[$change.Framework][$change.Package] + if ($null -ne $backupEntry -and $null -ne $currentEntry -and $backupEntry.ContainsKey('resolved') -and $currentEntry.ContainsKey('resolved')) { + $backupVersion = $backupEntry.resolved + $currentVersion = $currentEntry.resolved + $versionInfo = " (version: $backupVersion -> $currentVersion)" + } + } elseif ($change.Type -eq 'Removed') { + $backupEntry = $backup.dependencies[$change.Framework][$change.Package] + if ($null -ne $backupEntry -and $backupEntry.ContainsKey('resolved')) { + $backupVersion = $backupEntry.resolved + $versionInfo = " (version: $backupVersion)" + } + } elseif ($change.Type -eq 'Added') { + $currentEntry = $current.dependencies[$change.Framework][$change.Package] + if ($null -ne $currentEntry -and $currentEntry.ContainsKey('resolved')) { + $currentVersion = $currentEntry.resolved + $versionInfo = " (version: $currentVersion)" + } + } + Write-Host " [$($change.Type)] $($change.Package)$versionInfo" + } + Write-Host [string]::Empty +} + +if ($unexpectedChanges.Count -gt 0) { + Write-Error ( + 'ERROR: Non-platform-specific packages changed in lock file! ' + + 'Only platform-specific Aspire packages (and the project entries that reference them) should differ between platforms.' + + "`nUnexpected changes detected:" + ($unexpectedChanges | ForEach-Object { "`n - $_" }) + ) + exit 1 +} + +if ($expectedChanges.Count -eq 0) { + Write-Host 'No changes detected in lock file' +} else { + Write-Host 'Lock file updated successfully - only platform-specific packages changed' +} + +exit 0 diff --git a/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj b/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj index 14c2500..ae52c1e 100644 --- a/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj +++ b/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj @@ -30,7 +30,7 @@ - + diff --git a/src/Installer/Msi/CustomActions/packages.lock.json b/src/Installer/Msi/CustomActions/packages.lock.json index 52e6310..9d0c1f5 100644 --- a/src/Installer/Msi/CustomActions/packages.lock.json +++ b/src/Installer/Msi/CustomActions/packages.lock.json @@ -1,107 +1,107 @@ -{ - "version": 1, - "dependencies": { - ".NETFramework,Version=v4.8.1": { - "System.Text.Json": { - "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "10.0.4", - "System.Buffers": "4.6.1", - "System.IO.Pipelines": "10.0.4", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2", - "System.Text.Encodings.Web": "10.0.4", - "System.Threading.Tasks.Extensions": "4.6.3", - "System.ValueTuple": "4.6.1" - } - }, - "WixToolset.Dtf.CustomAction": { - "type": "Direct", - "requested": "[6.0.2, )", - "resolved": "6.0.2", - "contentHash": "VJRjIOzIkfolXw+kUWenyx2YQT/gagpr6Cs0XnvLPz7xl6pp/v+lQdwWh+wBu2P6apkg4yH1XI1WmXrTCjlD9g==", - "dependencies": { - "WixToolset.Dtf.WindowsInstaller": "6.0.2" - } - }, - "WixToolset.Dtf.WindowsInstaller": { - "type": "Direct", - "requested": "[6.0.2, )", - "resolved": "6.0.2", - "contentHash": "Tnc1EIjE5A7nvEcnUQLEgf1F1sXWzd3L4/n1/PXMnlt7dzaTEvR2OTiFgHD6Y+Tq9qZzAQZelcoS0Ps9bCLufw==" - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "2JmoSZ1wDf1/TUyTtLTXeicXCnWxXkeStGnzRRmAw+5CBIGhg6q9ieJXu4FjeLzawSGd5PMhcropNa3lPJDaKA==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.6.3" - } - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.6.1", - "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==", - "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3", - "System.Threading.Tasks.Extensions": "4.6.3" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", - "dependencies": { - "System.Buffers": "4.6.1", - "System.Numerics.Vectors": "4.6.1", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" - } - }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.6.1", - "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.1.2", - "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==", - "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.1.2" - } - }, - "System.ValueTuple": { - "type": "Transitive", - "resolved": "4.6.1", - "contentHash": "+RJT4qaekpZ7DDLhf+LTjq+E48jieKiY9ulJ+BoxKmZblIJfIJT8Ufcaa/clQqnYvWs8jugfGSMu8ylS0caG0w==" - }, - "werkr.common.configuration": { - "type": "Project" - } - } - } +{ + "version": 1, + "dependencies": { + ".NETFramework,Version=v4.8.1": { + "System.Text.Json": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.5", + "System.Buffers": "4.6.1", + "System.IO.Pipelines": "10.0.5", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.Text.Encodings.Web": "10.0.5", + "System.Threading.Tasks.Extensions": "4.6.3", + "System.ValueTuple": "4.6.1" + } + }, + "WixToolset.Dtf.CustomAction": { + "type": "Direct", + "requested": "[6.0.2, )", + "resolved": "6.0.2", + "contentHash": "VJRjIOzIkfolXw+kUWenyx2YQT/gagpr6Cs0XnvLPz7xl6pp/v+lQdwWh+wBu2P6apkg4yH1XI1WmXrTCjlD9g==", + "dependencies": { + "WixToolset.Dtf.WindowsInstaller": "6.0.2" + } + }, + "WixToolset.Dtf.WindowsInstaller": { + "type": "Direct", + "requested": "[6.0.2, )", + "resolved": "6.0.2", + "contentHash": "Tnc1EIjE5A7nvEcnUQLEgf1F1sXWzd3L4/n1/PXMnlt7dzaTEvR2OTiFgHD6Y+Tq9qZzAQZelcoS0Ps9bCLufw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "hQB3Hq1LlF0NkGVNyZIvwIQIY3LM7Cw1oYjNiTvdNqmzzipVAWEK1c5sj2H5aFX0udnjgPLxSYKq2fupueS8ow==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "+RJT4qaekpZ7DDLhf+LTjq+E48jieKiY9ulJ+BoxKmZblIJfIJT8Ufcaa/clQqnYvWs8jugfGSMu8ylS0caG0w==" + }, + "werkr.common.configuration": { + "type": "Project" + } + } + } } \ No newline at end of file diff --git a/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs b/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs index 9c7af21..d7b3816 100644 --- a/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs +++ b/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs @@ -97,7 +97,10 @@ public void RelativeUrl_Rejected( ) { UnauthorizedAccessException ex = Assert.ThrowsExactly( ( ) => validator.ValidateUrl( "/api/data" ) ); - Assert.Contains( "absolute", ex.Message ); + // On Unix, Uri.TryCreate parses "/api/data" as file:///api/data (hitting scheme check). + // On Windows, it fails to parse (hitting the absolute-URI check). + bool matchesAbsoluteOrScheme = ex.Message.Contains( "absolute" ) || ex.Message.Contains( "scheme" ); + Assert.IsTrue( matchesAbsoluteOrScheme, $"Expected 'absolute' or 'scheme' in message: {ex.Message}" ); } // ══════════════════════════════════════════════════════════════════════════════ diff --git a/src/Test/Werkr.Tests.Agent/packages.lock.json b/src/Test/Werkr.Tests.Agent/packages.lock.json index 5be9d1d..ce52fe3 100644 --- a/src/Test/Werkr.Tests.Agent/packages.lock.json +++ b/src/Test/Werkr.Tests.Agent/packages.lock.json @@ -146,8 +146,8 @@ }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w==" + "resolved": "10.0.5", + "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A==" }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", @@ -188,36 +188,36 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", "SQLitePCLRaw.core": "2.1.11" } }, @@ -233,22 +233,22 @@ }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Caching.Memory": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Compliance.Abstractions": { @@ -262,46 +262,46 @@ }, "Microsoft.Extensions.Configuration": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==", + "resolved": "10.0.5", + "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ==" + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DependencyInjection.AutoActivation": { "type": "Transitive", @@ -313,26 +313,26 @@ }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==", + "resolved": "10.0.5", + "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { @@ -350,26 +350,26 @@ }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==", + "resolved": "10.0.5", + "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileSystemGlobbing": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg==" + "resolved": "10.0.5", + "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Http": { "type": "Transitive", @@ -395,12 +395,12 @@ }, "Microsoft.Extensions.Logging": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Configuration": { @@ -425,29 +425,29 @@ }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==", + "resolved": "10.0.5", + "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg==" + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.Extensions.Resilience": { "type": "Transitive", @@ -799,8 +799,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } @@ -1321,8 +1321,8 @@ "type": "Project", "dependencies": { "Google.Protobuf": "[3.34.0, )", - "Microsoft.AspNetCore.Authorization": "[10.0.4, )", - "Microsoft.Extensions.Configuration.Json": "[10.0.4, )", + "Microsoft.AspNetCore.Authorization": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Json": "[10.0.5, )", "Microsoft.IdentityModel.Tokens": "[8.16.0, )", "Werkr.Common.Configuration": "[1.0.0, )" } @@ -1334,8 +1334,8 @@ "type": "Project", "dependencies": { "Grpc.Net.Client": "[2.76.0, )", - "Microsoft.Extensions.Hosting.Abstractions": "[10.0.4, )", - "System.Security.Cryptography.ProtectedData": "[10.0.4, )", + "Microsoft.Extensions.Hosting.Abstractions": "[10.0.5, )", + "System.Security.Cryptography.ProtectedData": "[10.0.5, )", "Werkr.Common": "[1.0.0, )", "Werkr.Data": "[1.0.0, )" } @@ -1344,9 +1344,9 @@ "type": "Project", "dependencies": { "EFCore.NamingConventions": "[10.0.1, )", - "Microsoft.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", "Werkr.Common": "[1.0.0, )" } }, @@ -1407,6 +1407,12 @@ "Microsoft.Extensions.Http": "8.0.0" } }, + "Grpc.Tools": { + "type": "CentralTransitive", + "requested": "[2.78.0, )", + "resolved": "2.78.0", + "contentHash": "6jPG2gHon+w2PczW8jjrCRnW/g9eEfCdd7aK6mDooptWtuPsV3ZxAwKKEx7LGEDVoT4c2SViRl8Yu3L1XiWIIg==" + }, "MailKit": { "type": "CentralTransitive", "requested": "[4.15.1, )", @@ -1418,84 +1424,84 @@ }, "Microsoft.AspNetCore.Authorization": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==", "dependencies": { - "Microsoft.AspNetCore.Metadata": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" + "Microsoft.AspNetCore.Metadata": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Data.Sqlite.Core": { "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", "dependencies": { "SQLitePCLRaw.core": "2.1.11" } }, "Microsoft.EntityFrameworkCore": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", "SQLitePCLRaw.core": "2.1.11" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "+5mQrqlBhqNUaPyDmFSNM/qiWStvE9LMxZW1MRF0NhEbO781xNeKryXNR9gGDJ0PmYFDAVoMT9ffX+I15LPFTw==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Http.Resilience": { @@ -1511,11 +1517,11 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.ServiceDiscovery": { @@ -1540,13 +1546,13 @@ }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" } }, "OpenTelemetry.Exporter.OpenTelemetryProtocol": { @@ -1614,9 +1620,9 @@ }, "System.Security.Cryptography.ProtectedData": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg==" } } } diff --git a/src/Test/Werkr.Tests.Data/Unit/Communication/WorkflowEventBroadcasterTests.cs b/src/Test/Werkr.Tests.Data/Unit/Communication/WorkflowEventBroadcasterTests.cs new file mode 100644 index 0000000..ba42093 --- /dev/null +++ b/src/Test/Werkr.Tests.Data/Unit/Communication/WorkflowEventBroadcasterTests.cs @@ -0,0 +1,153 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Werkr.Core.Communication; + +namespace Werkr.Tests.Data.Unit.Communication; + +/// +/// Unit tests for the class, validating subscription management, event +/// fan-out delivery, and thread-safe subscriber count tracking. +/// +[TestClass] +public class WorkflowEventBroadcasterTests { + /// + /// The instance under test. + /// + private WorkflowEventBroadcaster _broadcaster = null!; + + /// + /// Gets or sets the MSTest providing per-test cancellation tokens and metadata. + /// + public TestContext TestContext { get; set; } = null!; + + /// + /// Creates a fresh for each test. + /// + [TestInitialize] + public void TestInit( ) { + _broadcaster = new WorkflowEventBroadcaster( + NullLogger.Instance + ); + } + + /// + /// Verifies that returns a non-null subscription + /// with a readable channel. + /// + [TestMethod] + public void Subscribe_ReturnsSubscriptionWithReader( ) { + using WorkflowEventSubscription sub = _broadcaster.Subscribe( ); + + Assert.IsNotNull( sub ); + Assert.IsNotNull( sub.Reader ); + } + + /// + /// Verifies that published events are delivered to a single subscriber. + /// + [TestMethod] + public async Task Publish_DeliversEventToSubscriber( ) { + CancellationToken ct = TestContext.CancellationToken; + using WorkflowEventSubscription sub = _broadcaster.Subscribe( ); + + Guid runId = Guid.NewGuid( ); + StepStartedEvent evt = new( runId, 1, "Step1", 10, DateTime.UtcNow ); + _broadcaster.Publish( evt ); + + bool available = await sub.Reader.WaitToReadAsync( ct ); + + Assert.IsTrue( available ); + Assert.IsTrue( sub.Reader.TryRead( out WorkflowEvent? received ) ); + Assert.AreEqual( runId, received!.WorkflowRunId ); + } + + /// + /// Verifies that published events fan out to all active subscribers. + /// + [TestMethod] + public async Task Publish_FansOutToMultipleSubscribers( ) { + CancellationToken ct = TestContext.CancellationToken; + using WorkflowEventSubscription sub1 = _broadcaster.Subscribe( ); + using WorkflowEventSubscription sub2 = _broadcaster.Subscribe( ); + + Guid runId = Guid.NewGuid( ); + StepStartedEvent evt = new( runId, 1, "Step1", 10, DateTime.UtcNow ); + _broadcaster.Publish( evt ); + + Assert.IsTrue( await sub1.Reader.WaitToReadAsync( ct ) ); + Assert.IsTrue( sub1.Reader.TryRead( out WorkflowEvent? received1 ) ); + Assert.AreEqual( runId, received1!.WorkflowRunId ); + + Assert.IsTrue( await sub2.Reader.WaitToReadAsync( ct ) ); + Assert.IsTrue( sub2.Reader.TryRead( out WorkflowEvent? received2 ) ); + Assert.AreEqual( runId, received2!.WorkflowRunId ); + } + + /// + /// Verifies that disposing a subscription removes it from the broadcaster so + /// subsequent publishes are not delivered to the disposed subscriber. + /// + [TestMethod] + public void Dispose_RemovesSubscriber( ) { + WorkflowEventSubscription sub = _broadcaster.Subscribe( ); + sub.Dispose( ); + + Guid runId = Guid.NewGuid( ); + StepStartedEvent evt = new( runId, 1, "Step1", 10, DateTime.UtcNow ); + _broadcaster.Publish( evt ); + + // Channel was completed on unsubscribe; TryRead should return false. + Assert.IsFalse( sub.Reader.TryRead( out _ ) ); + } + + /// + /// Verifies that concurrent subscribe and unsubscribe operations do not corrupt + /// the subscriber list. + /// + [TestMethod] + public async Task ConcurrentSubscribeUnsubscribe_DoesNotCorruptState( ) { + CancellationToken ct = TestContext.CancellationToken; + const int Iterations = 100; + List tasks = []; + + for (int i = 0; i < Iterations; i++) { + tasks.Add( Task.Run( ( ) => { + WorkflowEventSubscription sub = _broadcaster.Subscribe( ); + sub.Dispose( ); + }, ct ) ); + } + + await Task.WhenAll( tasks ); + + // After all subscribe/unsubscribe pairs complete, a new publish should succeed + // without throwing (no corrupted list). + using WorkflowEventSubscription final = _broadcaster.Subscribe( ); + Guid runId = Guid.NewGuid( ); + StepStartedEvent evt = new( runId, 1, "Step1", 10, DateTime.UtcNow ); + _broadcaster.Publish( evt ); + + Assert.IsTrue( await final.Reader.WaitToReadAsync( ct ) ); + Assert.IsTrue( final.Reader.TryRead( out WorkflowEvent? received ) ); + Assert.AreEqual( runId, received!.WorkflowRunId ); + } + + /// + /// Verifies that publishing with no subscribers does not throw. + /// + [TestMethod] + public void Publish_WithNoSubscribers_DoesNotThrow( ) { + Guid runId = Guid.NewGuid( ); + StepStartedEvent evt = new( runId, 1, "Step1", 10, DateTime.UtcNow ); + + _broadcaster.Publish( evt ); + } + + /// + /// Verifies that disposing a subscription twice does not throw. + /// + [TestMethod] + public void Dispose_CalledTwice_DoesNotThrow( ) { + WorkflowEventSubscription sub = _broadcaster.Subscribe( ); + sub.Dispose( ); + sub.Dispose( ); + } +} diff --git a/src/Test/Werkr.Tests.Data/Unit/Endpoints/FilterEndpointTests.cs b/src/Test/Werkr.Tests.Data/Unit/Endpoints/FilterEndpointTests.cs new file mode 100644 index 0000000..e7ffc0c --- /dev/null +++ b/src/Test/Werkr.Tests.Data/Unit/Endpoints/FilterEndpointTests.cs @@ -0,0 +1,254 @@ +using System.Reflection; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Werkr.Data; +using Werkr.Data.Entities.Settings; + +namespace Werkr.Tests.Data.Unit.Endpoints; + +/// +/// Unit tests for the FilterEndpoints class, validating page key allowlist completeness +/// and that filter CRUD operations enforce ownership and produce correct persistence results. +/// +[TestClass] +public class FilterEndpointTests { + /// + /// The in-memory SQLite connection used for database operations. + /// + private SqliteConnection _connection = null!; + /// + /// The SQLite-backed used for test data persistence. + /// + private SqliteWerkrDbContext _dbContext = null!; + + /// + /// Gets or sets the MSTest providing per-test cancellation tokens and metadata. + /// + public TestContext TestContext { get; set; } = null!; + + /// + /// The complete set of valid page keys that the filter endpoints must accept. + /// + private static readonly HashSet s_expectedPageKeys = [ + "runs", "workflows", "jobs", "agents", "schedules", "tasks", + "all-workflow-runs", "workflow-dashboard" + ]; + + /// + /// Creates an in-memory SQLite database and the schema for each test. + /// + [TestInitialize] + public void TestInit( ) { + _connection = new SqliteConnection( "DataSource=:memory:" ); + _connection.Open( ); + + DbContextOptions options = new DbContextOptionsBuilder( ) + .UseSqlite( _connection ) + .Options; + + _dbContext = new SqliteWerkrDbContext( options ); + _ = _dbContext.Database.EnsureCreated( ); + } + + /// + /// Disposes the database context and SQLite connection after each test. + /// + [TestCleanup] + public void TestCleanup( ) { + _dbContext?.Dispose( ); + _connection?.Dispose( ); + } + + /// + /// Verifies that the s_validPageKeys field on FilterEndpoints contains exactly the + /// expected set of page keys including all-workflow-runs and workflow-dashboard. + /// + [TestMethod] + public void ValidPageKeys_ContainsAllExpectedKeys( ) { + Assembly apiAssembly = Assembly.Load( "Werkr.Api" ); + Type? endpointsType = apiAssembly.GetType( "Werkr.Api.Endpoints.FilterEndpoints" ); + Assert.IsNotNull( endpointsType, "FilterEndpoints type not found in Werkr.Api assembly" ); + + FieldInfo? field = endpointsType.GetField( + "s_validPageKeys", + BindingFlags.NonPublic | BindingFlags.Static + ); + + Assert.IsNotNull( field, "s_validPageKeys field not found on FilterEndpoints" ); + + object? value = field.GetValue( null ); + _ = Assert.IsInstanceOfType>( value ); + + HashSet actualKeys = (HashSet)value; + + foreach (string expected in s_expectedPageKeys) { + Assert.Contains( + expected, actualKeys, + $"Missing page key: '{expected}'" + ); + } + + Assert.HasCount( + s_expectedPageKeys.Count, + actualKeys, + $"Page key count mismatch. Expected: {s_expectedPageKeys.Count}, Actual: {actualKeys.Count}" + ); + } + + /// + /// Verifies that creating a filter persists the entity with the correct owner and page key. + /// + [TestMethod] + public async Task CreateFilter_PersistsWithCorrectOwnerAndPageKey( ) { + CancellationToken ct = TestContext.CancellationToken; + string userId = "user-1"; + + SavedFilter entity = new( ) { + OwnerId = userId, + PageKey = "runs", + Name = "My Filter", + CriteriaJson = "{\"status\":\"Running\"}", + IsShared = false, + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + _ = _dbContext.SavedFilters.Add( entity ); + _ = await _dbContext.SaveChangesAsync( ct ); + + SavedFilter? loaded = await _dbContext.SavedFilters + .FirstOrDefaultAsync( f => f.OwnerId == userId && f.PageKey == "runs", ct ); + + Assert.IsNotNull( loaded ); + Assert.AreEqual( "My Filter", loaded.Name ); + Assert.AreEqual( "{\"status\":\"Running\"}", loaded.CriteriaJson ); + Assert.IsFalse( loaded.IsShared ); + } + + /// + /// Verifies that filters can be queried by page key and include both owned and shared filters. + /// + [TestMethod] + public async Task QueryFilters_ReturnsBothOwnedAndShared( ) { + CancellationToken ct = TestContext.CancellationToken; + string userId = "user-1"; + + SavedFilter owned = new( ) { + OwnerId = userId, + PageKey = "runs", + Name = "My Filter", + CriteriaJson = "{}", + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + SavedFilter shared = new( ) { + OwnerId = "other-user", + PageKey = "runs", + Name = "Shared Filter", + CriteriaJson = "{}", + IsShared = true, + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + SavedFilter differentPage = new( ) { + OwnerId = userId, + PageKey = "jobs", + Name = "Jobs Filter", + CriteriaJson = "{}", + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + _dbContext.SavedFilters.AddRange( owned, shared, differentPage ); + _ = await _dbContext.SaveChangesAsync( ct ); + + List results = await _dbContext.SavedFilters + .Where( f => f.PageKey == "runs" && (f.OwnerId == userId || f.IsShared) ) + .OrderBy( f => f.Name ) + .ToListAsync( ct ); + + Assert.HasCount( 2, results ); + Assert.AreEqual( "My Filter", results[0].Name ); + Assert.AreEqual( "Shared Filter", results[1].Name ); + } + + /// + /// Verifies that deleting a filter only removes the targeted entity. + /// + [TestMethod] + public async Task DeleteFilter_RemovesOnlyTargetEntity( ) { + CancellationToken ct = TestContext.CancellationToken; + + SavedFilter filter1 = new( ) { + OwnerId = "user-1", + PageKey = "runs", + Name = "Filter 1", + CriteriaJson = "{}", + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + SavedFilter filter2 = new( ) { + OwnerId = "user-1", + PageKey = "runs", + Name = "Filter 2", + CriteriaJson = "{}", + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + _dbContext.SavedFilters.AddRange( filter1, filter2 ); + _ = await _dbContext.SaveChangesAsync( ct ); + + _ = _dbContext.SavedFilters.Remove( filter1 ); + _ = await _dbContext.SaveChangesAsync( ct ); + + List remaining = await _dbContext.SavedFilters.ToListAsync( ct ); + + Assert.HasCount( 1, remaining ); + Assert.AreEqual( "Filter 2", remaining[0].Name ); + } + + /// + /// Verifies that updating a filter increments the version and persists the new values. + /// + [TestMethod] + public async Task UpdateFilter_IncrementsVersionAndPersists( ) { + CancellationToken ct = TestContext.CancellationToken; + + SavedFilter entity = new( ) { + OwnerId = "user-1", + PageKey = "runs", + Name = "Original", + CriteriaJson = "{}", + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + _ = _dbContext.SavedFilters.Add( entity ); + _ = await _dbContext.SaveChangesAsync( ct ); + + entity.Name = "Updated"; + entity.CriteriaJson = "{\"status\":\"Failed\"}"; + entity.Version++; + entity.LastUpdated = DateTime.UtcNow; + _ = await _dbContext.SaveChangesAsync( ct ); + + SavedFilter? loaded = await _dbContext.SavedFilters + .AsNoTracking( ) + .FirstOrDefaultAsync( f => f.Id == entity.Id, ct ); + + Assert.IsNotNull( loaded ); + Assert.AreEqual( "Updated", loaded.Name ); + Assert.AreEqual( "{\"status\":\"Failed\"}", loaded.CriteriaJson ); + } +} diff --git a/src/Test/Werkr.Tests.Data/Unit/Scheduling/RetryFromFailedServiceTests.cs b/src/Test/Werkr.Tests.Data/Unit/Scheduling/RetryFromFailedServiceTests.cs new file mode 100644 index 0000000..c0817f7 --- /dev/null +++ b/src/Test/Werkr.Tests.Data/Unit/Scheduling/RetryFromFailedServiceTests.cs @@ -0,0 +1,516 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Werkr.Common.Models; +using Werkr.Core.Scheduling; +using Werkr.Data; +using Werkr.Data.Entities.Tasks; +using Werkr.Data.Entities.Workflows; + +namespace Werkr.Tests.Data.Unit.Scheduling; + +/// +/// Unit tests for , validating retry-from-failed orchestration +/// including variable override batching, failed-step validation, and schedule creation. +/// +[TestClass] +public class RetryFromFailedServiceTests { + /// + /// The in-memory SQLite connection used for database operations. + /// + private SqliteConnection _connection = null!; + /// + /// The SQLite-backed used for test data persistence. + /// + private SqliteWerkrDbContext _dbContext = null!; + /// + /// The instance under test. + /// + private RetryFromFailedService _service = null!; + + /// + /// Gets or sets the MSTest providing per-test cancellation tokens and metadata. + /// + public TestContext TestContext { get; set; } = null!; + + /// + /// Creates an in-memory SQLite database, the schema, and the service under test. + /// + [TestInitialize] + public void TestInit( ) { + _connection = new SqliteConnection( "DataSource=:memory:" ); + _connection.Open( ); + + DbContextOptions options = new DbContextOptionsBuilder( ) + .UseSqlite( _connection ) + .Options; + + _dbContext = new SqliteWerkrDbContext( options ); + _ = _dbContext.Database.EnsureCreated( ); + + _service = new RetryFromFailedService( + _dbContext, + NullLogger.Instance + ); + } + + /// + /// Disposes the database context and SQLite connection after each test. + /// + [TestCleanup] + public void TestCleanup( ) { + _dbContext?.Dispose( ); + _connection?.Dispose( ); + } + + #region Helpers + + /// + /// Seeds a minimal workflow with a single task, step, run, and failed step execution. + /// Returns a tuple of (workflowId, runId, stepId). + /// + private async Task<(long WorkflowId, Guid RunId, long StepId)> SeedFailedRunAsync( + CancellationToken ct ) { + + Workflow workflow = new( ) { Name = "Test Workflow", Description = "Test" }; + _ = _dbContext.Set( ).Add( workflow ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WerkrTask task = new( ) { + Name = "Test Task", + WorkflowId = workflow.Id, + ActionType = TaskActionType.PowerShellCommand, + Content = "Write-Output 'test'", + TargetTags = ["default"], + }; + _ = _dbContext.Set( ).Add( task ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WorkflowStep step = new( ) { + WorkflowId = workflow.Id, + TaskId = task.Id, + Order = 1, + }; + _ = _dbContext.WorkflowSteps.Add( step ); + _ = await _dbContext.SaveChangesAsync( ct ); + + Guid runId = Guid.NewGuid( ); + WorkflowRun run = new( ) { + Id = runId, + WorkflowId = workflow.Id, + StartTime = DateTime.UtcNow.AddMinutes( -5 ), + EndTime = DateTime.UtcNow.AddMinutes( -1 ), + Status = WorkflowRunStatus.Failed, + }; + _ = _dbContext.WorkflowRuns.Add( run ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WorkflowStepExecution failedExecution = new( ) { + WorkflowRunId = runId, + StepId = step.Id, + Attempt = 1, + Status = StepExecutionStatus.Failed, + }; + _ = _dbContext.WorkflowStepExecutions.Add( failedExecution ); + _ = await _dbContext.SaveChangesAsync( ct ); + + return (workflow.Id, runId, step.Id); + } + + #endregion + + /// + /// Verifies that retry succeeds for a valid failed run and creates a new schedule. + /// + [TestMethod] + public async Task RetryAsync_ValidFailedRun_ReturnsResult( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct ); + + RetryFromFailedService.RetryResult result = await _service.RetryAsync( + workflowId, runId, stepId, null, ct + ); + + Assert.AreEqual( runId, result.RunId ); + Assert.AreEqual( stepId, result.RetryFromStepId ); + Assert.AreEqual( 1, result.ResetStepCount ); + } + + /// + /// Verifies that retry transitions the run from Failed to Running. + /// + [TestMethod] + public async Task RetryAsync_TransitionsRunToRunning( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct ); + + _ = await _service.RetryAsync( workflowId, runId, stepId, null, ct ); + + WorkflowRun run = await _dbContext.WorkflowRuns.AsNoTracking( ).FirstAsync( r => r.Id == runId, ct ); + + Assert.AreEqual( WorkflowRunStatus.Running, run.Status ); + Assert.IsNull( run.EndTime ); + } + + /// + /// Verifies that retry creates a new Pending step execution with an incremented attempt number. + /// + [TestMethod] + public async Task RetryAsync_CreatesNewPendingExecution( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct ); + + _ = await _service.RetryAsync( workflowId, runId, stepId, null, ct ); + + List executions = await _dbContext.WorkflowStepExecutions + .Where( e => e.WorkflowRunId == runId && e.StepId == stepId ) + .OrderBy( e => e.Attempt ) + .ToListAsync( ct ); + + Assert.HasCount( 2, executions ); + Assert.AreEqual( StepExecutionStatus.Failed, executions[0].Status ); + Assert.AreEqual( 1, executions[0].Attempt ); + Assert.AreEqual( StepExecutionStatus.Pending, executions[1].Status ); + Assert.AreEqual( 2, executions[1].Attempt ); + } + + /// + /// Verifies that variable overrides are persisted with incremented versions using + /// the batch query (not N+1). + /// + [TestMethod] + public async Task RetryAsync_WithVariableOverrides_PersistsNewVersions( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct ); + + // Seed existing variables (version 1) + WorkflowRunVariable existingVar1 = new( ) { + WorkflowRunId = runId, + VariableName = "Env", + Value = "staging", + Version = 1, + Source = VariableSource.Default, + Created = DateTime.UtcNow, + }; + WorkflowRunVariable existingVar2 = new( ) { + WorkflowRunId = runId, + VariableName = "Retries", + Value = "3", + Version = 1, + Source = VariableSource.Default, + Created = DateTime.UtcNow, + }; + _dbContext.Set( ).AddRange( existingVar1, existingVar2 ); + _ = await _dbContext.SaveChangesAsync( ct ); + + Dictionary overrides = new( ) { + ["Env"] = "production", + ["Retries"] = "5", + }; + + _ = await _service.RetryAsync( workflowId, runId, stepId, overrides, ct ); + + List envVars = await _dbContext.Set( ) + .Where( v => v.WorkflowRunId == runId && v.VariableName == "Env" ) + .OrderBy( v => v.Version ) + .ToListAsync( ct ); + + Assert.HasCount( 2, envVars ); + Assert.AreEqual( 1, envVars[0].Version ); + Assert.AreEqual( "staging", envVars[0].Value ); + Assert.AreEqual( 2, envVars[1].Version ); + Assert.AreEqual( "production", envVars[1].Value ); + Assert.AreEqual( VariableSource.ReExecutionEdit, envVars[1].Source ); + + List retriesVars = await _dbContext.Set( ) + .Where( v => v.WorkflowRunId == runId && v.VariableName == "Retries" ) + .OrderBy( v => v.Version ) + .ToListAsync( ct ); + + Assert.HasCount( 2, retriesVars ); + Assert.AreEqual( 2, retriesVars[1].Version ); + Assert.AreEqual( "5", retriesVars[1].Value ); + } + + /// + /// Verifies that overrides for new variables (no prior version) start at version 1. + /// + [TestMethod] + public async Task RetryAsync_WithNewVariable_StartsAtVersionOne( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct ); + + Dictionary overrides = new( ) { + ["NewVar"] = "hello", + }; + + _ = await _service.RetryAsync( workflowId, runId, stepId, overrides, ct ); + + WorkflowRunVariable? newVar = await _dbContext.Set( ) + .FirstOrDefaultAsync( v => v.WorkflowRunId == runId && v.VariableName == "NewVar", ct ); + + Assert.IsNotNull( newVar ); + Assert.AreEqual( 1, newVar.Version ); + Assert.AreEqual( "hello", newVar.Value ); + Assert.AreEqual( VariableSource.ReExecutionEdit, newVar.Source ); + } + + /// + /// Verifies that retrying a run not in Failed status throws . + /// + [TestMethod] + public async Task RetryAsync_RunNotFailed_Throws( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct ); + + // Transition the run to Running first + WorkflowRun run = await _dbContext.WorkflowRuns.FirstAsync( r => r.Id == runId, ct ); + run.Status = WorkflowRunStatus.Running; + _ = await _dbContext.SaveChangesAsync( ct ); + + _ = await Assert.ThrowsExactlyAsync( ( ) => + _service.RetryAsync( workflowId, runId, stepId, null, ct ) + ); + } + + /// + /// Verifies that retrying with a step that doesn't belong to the workflow throws + /// . + /// + [TestMethod] + public async Task RetryAsync_StepNotInWorkflow_Throws( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, _) = await SeedFailedRunAsync( ct ); + + _ = await Assert.ThrowsExactlyAsync( ( ) => + _service.RetryAsync( workflowId, runId, 99999, null, ct ) + ); + } + + /// + /// Verifies that retrying with a mismatched workflowId (run belongs to a different workflow) + /// throws without modifying the run status. + /// + [TestMethod] + public async Task RetryAsync_WorkflowIdMismatch_Throws( ) { + CancellationToken ct = TestContext.CancellationToken; + (_, Guid runId, long stepId) = await SeedFailedRunAsync( ct ); + + long wrongWorkflowId = 99999; + _ = await Assert.ThrowsExactlyAsync( ( ) => + _service.RetryAsync( wrongWorkflowId, runId, stepId, null, ct ) + ); + + // Verify the run status was NOT changed. + WorkflowRun run = await _dbContext.WorkflowRuns.FirstAsync(r => r.Id == runId, ct); + Assert.AreEqual( WorkflowRunStatus.Failed, run.Status ); + } + + /// + /// Verifies that retrying from a step without a Failed execution throws + /// . + /// + [TestMethod] + public async Task RetryAsync_StepNotFailed_Throws( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct ); + + // Change the execution status to Completed + WorkflowStepExecution exec = await _dbContext.WorkflowStepExecutions + .FirstAsync( e => e.WorkflowRunId == runId && e.StepId == stepId, ct ); + exec.Status = StepExecutionStatus.Completed; + _ = await _dbContext.SaveChangesAsync( ct ); + + _ = await Assert.ThrowsExactlyAsync( ( ) => + _service.RetryAsync( workflowId, runId, stepId, null, ct ) + ); + } + + /// + /// Verifies that retry creates a one-time schedule linked to the workflow. + /// + [TestMethod] + public async Task RetryAsync_CreatesOneTimeSchedule( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct ); + + RetryFromFailedService.RetryResult result = await _service.RetryAsync( + workflowId, runId, stepId, null, ct + ); + + WorkflowSchedule? link = await _dbContext.WorkflowSchedules + .FirstOrDefaultAsync( ws => ws.ScheduleId == result.ScheduleId, ct ); + + Assert.IsNotNull( link ); + Assert.AreEqual( workflowId, link.WorkflowId ); + Assert.IsTrue( link.IsOneTime ); + Assert.AreEqual( runId, link.WorkflowRunId ); + } + + #region DAG and Repeated-Retry Tests + + /// + /// Seeds a 3-step DAG: A → B → C with B in Failed status and A/C in Completed/Pending. + /// Returns (workflowId, runId, stepAId, stepBId, stepCId). + /// + private async Task<(long WorkflowId, Guid RunId, long StepAId, long StepBId, long StepCId)> SeedDagFailedRunAsync( + CancellationToken ct ) { + + Workflow workflow = new() { Name = "DAG Workflow", Description = "A→B→C" }; + _ = _dbContext.Set( ).Add( workflow ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WerkrTask taskA = new() + { + Name = "Task A", + WorkflowId = workflow.Id, + ActionType = TaskActionType.PowerShellCommand, + Content = "echo A", + TargetTags = ["default"], + }; + WerkrTask taskB = new() + { + Name = "Task B", + WorkflowId = workflow.Id, + ActionType = TaskActionType.PowerShellCommand, + Content = "echo B", + TargetTags = ["default"], + }; + WerkrTask taskC = new() + { + Name = "Task C", + WorkflowId = workflow.Id, + ActionType = TaskActionType.PowerShellCommand, + Content = "echo C", + TargetTags = ["default"], + }; + _dbContext.Set( ).AddRange( taskA, taskB, taskC ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WorkflowStep stepA = new() { WorkflowId = workflow.Id, TaskId = taskA.Id, Order = 1 }; + WorkflowStep stepB = new() { WorkflowId = workflow.Id, TaskId = taskB.Id, Order = 2 }; + WorkflowStep stepC = new() { WorkflowId = workflow.Id, TaskId = taskC.Id, Order = 3 }; + _dbContext.WorkflowSteps.AddRange( stepA, stepB, stepC ); + _ = await _dbContext.SaveChangesAsync( ct ); + + // Dependencies: B depends on A, C depends on B + _dbContext.WorkflowStepDependencies.AddRange( + new WorkflowStepDependency { StepId = stepB.Id, DependsOnStepId = stepA.Id }, + new WorkflowStepDependency { StepId = stepC.Id, DependsOnStepId = stepB.Id } + ); + _ = await _dbContext.SaveChangesAsync( ct ); + + Guid runId = Guid.NewGuid(); + WorkflowRun run = new() + { + Id = runId, + WorkflowId = workflow.Id, + StartTime = DateTime.UtcNow.AddMinutes(-5), + EndTime = DateTime.UtcNow.AddMinutes(-1), + Status = WorkflowRunStatus.Failed, + }; + _ = _dbContext.WorkflowRuns.Add( run ); + _ = await _dbContext.SaveChangesAsync( ct ); + + // A = Completed (attempt 1), B = Failed (attempt 1), C = Pending (attempt 1) + _dbContext.WorkflowStepExecutions.AddRange( + new WorkflowStepExecution { WorkflowRunId = runId, StepId = stepA.Id, Attempt = 1, Status = StepExecutionStatus.Completed }, + new WorkflowStepExecution { WorkflowRunId = runId, StepId = stepB.Id, Attempt = 1, Status = StepExecutionStatus.Failed }, + new WorkflowStepExecution { WorkflowRunId = runId, StepId = stepC.Id, Attempt = 1, Status = StepExecutionStatus.Pending } + ); + _ = await _dbContext.SaveChangesAsync( ct ); + + return (workflow.Id, runId, stepA.Id, stepB.Id, stepC.Id); + } + + /// + /// Verifies that retrying from step B in a DAG (A→B→C) resets B and C but not A. + /// + [TestMethod] + public async Task RetryAsync_DagDownstreamReset_ResetsOnlyTargetAndDownstream( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, long stepAId, long stepBId, long stepCId) = + await SeedDagFailedRunAsync( ct ); + + RetryFromFailedService.RetryResult result = await _service.RetryAsync( + workflowId, runId, stepBId, null, ct + ); + + // B and C should be reset (2 steps) + Assert.AreEqual( 2, result.ResetStepCount ); + + // Step A should NOT have a new execution — still just its original Completed one + List execA = await _dbContext.WorkflowStepExecutions + .Where(e => e.WorkflowRunId == runId && e.StepId == stepAId) + .ToListAsync(ct); + Assert.HasCount( 1, execA ); + Assert.AreEqual( StepExecutionStatus.Completed, execA[0].Status ); + + // Step B should have attempt 1 (Failed) + attempt 2 (Pending) + List execB = await _dbContext.WorkflowStepExecutions + .Where(e => e.WorkflowRunId == runId && e.StepId == stepBId) + .OrderBy(e => e.Attempt) + .ToListAsync(ct); + Assert.HasCount( 2, execB ); + Assert.AreEqual( StepExecutionStatus.Failed, execB[0].Status ); + Assert.AreEqual( 1, execB[0].Attempt ); + Assert.AreEqual( StepExecutionStatus.Pending, execB[1].Status ); + Assert.AreEqual( 2, execB[1].Attempt ); + + // Step C should have attempt 1 (Pending, original) + attempt 2 (Pending, retry) + List execC = await _dbContext.WorkflowStepExecutions + .Where(e => e.WorkflowRunId == runId && e.StepId == stepCId) + .OrderBy(e => e.Attempt) + .ToListAsync(ct); + Assert.HasCount( 2, execC ); + Assert.AreEqual( 1, execC[0].Attempt ); + Assert.AreEqual( StepExecutionStatus.Pending, execC[1].Status ); + Assert.AreEqual( 2, execC[1].Attempt ); + } + + /// + /// Verifies that retrying twice increments attempt numbers correctly (1→2→3). + /// + [TestMethod] + public async Task RetryAsync_RepeatedRetry_IncrementsAttempts( ) { + CancellationToken ct = TestContext.CancellationToken; + (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct ); + + // First retry: attempt 1 (Failed) → creates attempt 2 (Pending) + _ = await _service.RetryAsync( workflowId, runId, stepId, null, ct ); + + // Simulate the retried execution failing again using ExecuteUpdateAsync + // (bypasses the change tracker, same as the service's CAS pattern). + _dbContext.ChangeTracker.Clear( ); + + _ = await _dbContext.WorkflowStepExecutions + .Where( e => e.WorkflowRunId == runId && e.StepId == stepId && e.Attempt == 2 ) + .ExecuteUpdateAsync( s => s.SetProperty( e => e.Status, StepExecutionStatus.Failed ), ct ); + + _ = await _dbContext.WorkflowRuns + .Where( r => r.Id == runId ) + .ExecuteUpdateAsync( s => s + .SetProperty( r => r.Status, WorkflowRunStatus.Failed ) + .SetProperty( r => r.EndTime, DateTime.UtcNow ), ct ); + + // Second retry: should create attempt 3 + _ = await _service.RetryAsync( workflowId, runId, stepId, null, ct ); + + _dbContext.ChangeTracker.Clear( ); + + List allExecs = await _dbContext.WorkflowStepExecutions + .Where(e => e.WorkflowRunId == runId && e.StepId == stepId) + .OrderBy(e => e.Attempt) + .ToListAsync(ct); + + Assert.HasCount( 3, allExecs ); + Assert.AreEqual( 1, allExecs[0].Attempt ); + Assert.AreEqual( StepExecutionStatus.Failed, allExecs[0].Status ); + Assert.AreEqual( 2, allExecs[1].Attempt ); + Assert.AreEqual( StepExecutionStatus.Failed, allExecs[1].Status ); + Assert.AreEqual( 3, allExecs[2].Attempt ); + Assert.AreEqual( StepExecutionStatus.Pending, allExecs[2].Status ); + } + + #endregion +} diff --git a/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs new file mode 100644 index 0000000..58f29ef --- /dev/null +++ b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs @@ -0,0 +1,233 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Werkr.Data; +using Werkr.Data.Entities.Tasks; +using Werkr.Data.Entities.Workflows; + +namespace Werkr.Tests.Data.Unit.Workflows; + +/// +/// Tests that the ControlStatementStringConverter in +/// correctly round-trips enum values through the database +/// as strings, including backward-compatible reading of the legacy "Sequential" value. +/// +[TestClass] +public class ControlStatementConverterTests { + private SqliteConnection _connection = null!; + private SqliteWerkrDbContext _dbContext = null!; + + public TestContext TestContext { get; set; } = null!; + + [TestInitialize] + public void TestInit( ) { + _connection = new SqliteConnection( "DataSource=:memory:" ); + _connection.Open( ); + + DbContextOptions options = new DbContextOptionsBuilder( ) + .UseSqlite( _connection ) + .UseSnakeCaseNamingConvention( ) + .Options; + + _dbContext = new SqliteWerkrDbContext( options ); + _ = _dbContext.Database.EnsureCreated( ); + } + + [TestCleanup] + public void TestCleanup( ) { + _dbContext?.Dispose( ); + _connection?.Dispose( ); + } + + /// + /// Creates a workflow step with , saves, reloads, + /// and verifies it round-trips correctly and is stored as the string "Default". + /// + [TestMethod] + public async Task Default_RoundTrips_AsDefaultString( ) { + CancellationToken ct = TestContext.CancellationToken; + + // Arrange — create a workflow and task to host the step + Workflow workflow = new( ) { Name = "RoundTrip_WF", Description = "test" }; + _ = _dbContext.Workflows.Add( workflow ); + WerkrTask task = new( ) { Name = "RoundTrip_Task", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; + _ = _dbContext.Tasks.Add( task ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WorkflowStep step = new( ) { + WorkflowId = workflow.Id, + TaskId = task.Id, + Order = 0, + ControlStatement = ControlStatement.Default, + }; + _ = _dbContext.WorkflowSteps.Add( step ); + _ = await _dbContext.SaveChangesAsync( ct ); + + // Detach so the next query hits the database + _dbContext.ChangeTracker.Clear( ); + + // Act — reload from DB + WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync( s => s.Id == step.Id, ct ); + + // Assert — enum value round-trips + Assert.AreEqual( ControlStatement.Default, loaded.ControlStatement ); + + // Assert — raw string in the database is "Default" + long stepId = step.Id; + string? raw = await _dbContext.Database + .SqlQuery( $"SELECT control_statement AS Value FROM workflow_steps WHERE id = {stepId}" ) + .SingleAsync( ct ); + Assert.AreEqual( "Default", raw ); + } + + /// + /// Verifies that every non-Default enum member round-trips through the database correctly. + /// + [TestMethod] + [DataRow( ControlStatement.If, "If" )] + [DataRow( ControlStatement.Else, "Else" )] + [DataRow( ControlStatement.ElseIf, "ElseIf" )] + [DataRow( ControlStatement.While, "While" )] + [DataRow( ControlStatement.Do, "Do" )] + public async Task AllEnumValues_RoundTrip_Correctly( ControlStatement value, string expectedString ) { + CancellationToken ct = TestContext.CancellationToken; + + Workflow workflow = new( ) { Name = $"RoundTrip_{value}", Description = "test" }; + _ = _dbContext.Workflows.Add( workflow ); + WerkrTask task = new( ) { Name = $"Task_{value}", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; + _ = _dbContext.Tasks.Add( task ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WorkflowStep step = new( ) { + WorkflowId = workflow.Id, + TaskId = task.Id, + Order = 0, + ControlStatement = value, + ConditionExpression = value is ControlStatement.If or ControlStatement.ElseIf or ControlStatement.While or ControlStatement.Do + ? "$? -eq $true" : null, + }; + _ = _dbContext.WorkflowSteps.Add( step ); + _ = await _dbContext.SaveChangesAsync( ct ); + + _dbContext.ChangeTracker.Clear( ); + + WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync( s => s.Id == step.Id, ct ); + Assert.AreEqual( value, loaded.ControlStatement ); + + long stepId = step.Id; + string? raw = await _dbContext.Database + .SqlQuery( $"SELECT control_statement AS Value FROM workflow_steps WHERE id = {stepId}" ) + .SingleAsync( ct ); + Assert.AreEqual( expectedString, raw ); + } + + /// + /// Verifies that the legacy "Sequential" string value in the database is correctly + /// read as by the converter. + /// + [TestMethod] + public async Task LegacySequential_ReadsAs_Default( ) { + CancellationToken ct = TestContext.CancellationToken; + + Workflow workflow = new( ) { Name = "Legacy_WF", Description = "test" }; + _ = _dbContext.Workflows.Add( workflow ); + WerkrTask task = new( ) { Name = "Legacy_Task", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; + _ = _dbContext.Tasks.Add( task ); + _ = await _dbContext.SaveChangesAsync( ct ); + + // Insert a step with "Default" first (to get a valid row) + WorkflowStep step = new( ) { + WorkflowId = workflow.Id, + TaskId = task.Id, + Order = 0, + ControlStatement = ControlStatement.Default, + }; + _ = _dbContext.WorkflowSteps.Add( step ); + _ = await _dbContext.SaveChangesAsync( ct ); + + // Manually overwrite the stored string to the legacy "Sequential" value + long stepId = step.Id; + _ = await _dbContext.Database.ExecuteSqlAsync( + $"UPDATE workflow_steps SET control_statement = 'Sequential' WHERE id = {stepId}", ct ); + + _dbContext.ChangeTracker.Clear( ); + + // Act — reload via EF + WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync( s => s.Id == step.Id, ct ); + + // Assert — converter maps "Sequential" → Default + Assert.AreEqual( ControlStatement.Default, loaded.ControlStatement ); + } + + /// + /// Verifies that all legacy database string values from the old ControlStatement enum + /// are correctly mapped to the current enum members by the converter. + /// + [TestMethod] + [DataRow( "Parallel", ControlStatement.Default )] + [DataRow( "ConditionalIf", ControlStatement.If )] + [DataRow( "ConditionalElseIf", ControlStatement.ElseIf )] + [DataRow( "ConditionalWhile", ControlStatement.While )] + [DataRow( "ConditionalDo", ControlStatement.Do )] + [DataRow( "ConditionalElse", ControlStatement.Else )] + public async Task LegacyValues_ReadAs_CorrectEnum( string legacyString, ControlStatement expected ) { + CancellationToken ct = TestContext.CancellationToken; + + Workflow workflow = new() { Name = $"Legacy_{legacyString}", Description = "test" }; + _ = _dbContext.Workflows.Add( workflow ); + WerkrTask task = new() { Name = $"Task_{legacyString}", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; + _ = _dbContext.Tasks.Add( task ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WorkflowStep step = new() + { + WorkflowId = workflow.Id, + TaskId = task.Id, + Order = 0, + ControlStatement = ControlStatement.Default, + }; + _ = _dbContext.WorkflowSteps.Add( step ); + _ = await _dbContext.SaveChangesAsync( ct ); + + long stepId = step.Id; + _ = await _dbContext.Database.ExecuteSqlAsync( + $"UPDATE workflow_steps SET control_statement = {legacyString} WHERE id = {stepId}", ct ); + + _dbContext.ChangeTracker.Clear( ); + + WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync(s => s.Id == step.Id, ct); + Assert.AreEqual( expected, loaded.ControlStatement ); + } + + /// + /// Verifies that an unrecognized string in the database falls back to . + /// + [TestMethod] + public async Task UnknownString_FallsBackTo_Default( ) { + CancellationToken ct = TestContext.CancellationToken; + + Workflow workflow = new() { Name = "Unknown_WF", Description = "test" }; + _ = _dbContext.Workflows.Add( workflow ); + WerkrTask task = new() { Name = "Unknown_Task", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; + _ = _dbContext.Tasks.Add( task ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WorkflowStep step = new() + { + WorkflowId = workflow.Id, + TaskId = task.Id, + Order = 0, + ControlStatement = ControlStatement.Default, + }; + _ = _dbContext.WorkflowSteps.Add( step ); + _ = await _dbContext.SaveChangesAsync( ct ); + + long stepId = step.Id; + _ = await _dbContext.Database.ExecuteSqlAsync( + $"UPDATE workflow_steps SET control_statement = 'Bogus' WHERE id = {stepId}", ct ); + + _dbContext.ChangeTracker.Clear( ); + + WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync(s => s.Id == step.Id, ct); + Assert.AreEqual( ControlStatement.Default, loaded.ControlStatement ); + } +} diff --git a/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs b/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs index 51e9fae..1b9b200 100644 --- a/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs +++ b/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs @@ -625,6 +625,96 @@ await _service.GetTopologicalLevelsAsync( ); // Level 1: B, C } + /// + /// Verifies that three independent steps (no dependencies) are all grouped at level 0. + /// + [TestMethod] + public async Task GetTopologicalLevelsAsync_ThreeIndependentSteps_AllAtLevelZero( ) { + CancellationToken ct = TestContext.CancellationToken; + Workflow workflow = new( ) { Name = "ThreeIndependent", Description = string.Empty }; + _ = await _service.CreateAsync( workflow, ct ); + await SeedTaskAsync( ct ); + + _ = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 0 }, ct ); + _ = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 1 }, ct ); + _ = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 2 }, ct ); + + IReadOnlyList> levels = + await _service.GetTopologicalLevelsAsync( workflow.Id, ct ); + + Assert.HasCount( 1, levels ); + Assert.HasCount( 3, levels[0] ); + } + + /// + /// Verifies a diamond-shaped DAG: A → B, A → C, B → D, C → D produces + /// three levels: [A], [B, C], [D]. B and C are parallelizable at level 1. + /// + [TestMethod] + public async Task GetTopologicalLevelsAsync_DiamondDag_GroupsCorrectly( ) { + CancellationToken ct = TestContext.CancellationToken; + Workflow workflow = new( ) { Name = "Diamond", Description = string.Empty }; + _ = await _service.CreateAsync( workflow, ct ); + await SeedTaskAsync( ct ); + + WorkflowStep a = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 0 }, ct ); + WorkflowStep b = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 1 }, ct ); + WorkflowStep c = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 2 }, ct ); + WorkflowStep d = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 3 }, ct ); + + await _service.AddStepDependencyAsync( b.Id, a.Id, ct ); + await _service.AddStepDependencyAsync( c.Id, a.Id, ct ); + await _service.AddStepDependencyAsync( d.Id, b.Id, ct ); + await _service.AddStepDependencyAsync( d.Id, c.Id, ct ); + + IReadOnlyList> levels = + await _service.GetTopologicalLevelsAsync( workflow.Id, ct ); + + Assert.HasCount( 3, levels ); + Assert.HasCount( 1, levels[0] ); // Level 0: A + Assert.HasCount( 2, levels[1] ); // Level 1: B, C (parallel) + Assert.HasCount( 1, levels[2] ); // Level 2: D + } + + /// + /// Verifies that If/ElseIf/Else steps at the same level are separated from Default steps + /// for independent chain-bound processing. Both sets share the same topological level + /// but the execution engine will partition them for sequential vs. parallel execution. + /// + [TestMethod] + public async Task GetTopologicalLevelsAsync_MixedControlStatements_SameLevelGrouped( ) { + CancellationToken ct = TestContext.CancellationToken; + Workflow workflow = new( ) { Name = "MixedControl", Description = string.Empty }; + _ = await _service.CreateAsync( workflow, ct ); + await SeedTaskAsync( ct ); + + // Root step + WorkflowStep root = await _service.AddStepAsync( + workflow.Id, new WorkflowStep { TaskId = 1, Order = 0 }, ct ); + + // Default step depends on root + WorkflowStep defaultStep = await _service.AddStepAsync( + workflow.Id, new WorkflowStep { TaskId = 1, Order = 1, ControlStatement = ControlStatement.Default }, ct ); + await _service.AddStepDependencyAsync( defaultStep.Id, root.Id, ct ); + + // If step depends on root + WorkflowStep ifStep = await _service.AddStepAsync( + workflow.Id, new WorkflowStep { TaskId = 1, Order = 2, ControlStatement = ControlStatement.If, ConditionExpression = "$? -eq $true" }, ct ); + await _service.AddStepDependencyAsync( ifStep.Id, root.Id, ct ); + + IReadOnlyList> levels = + await _service.GetTopologicalLevelsAsync( workflow.Id, ct ); + + Assert.HasCount( 2, levels ); + Assert.HasCount( 1, levels[0] ); // Level 0: root + Assert.HasCount( 2, levels[1] ); // Level 1: defaultStep + ifStep (execution engine partitions them) + + // Verify both steps are at level 1 with their correct control statements + HashSet controlStatements = [.. levels[1].Select( s => s.ControlStatement )]; + Assert.Contains( ControlStatement.Default, controlStatements ); + Assert.Contains( ControlStatement.If, controlStatements ); + } + // ── Control Flow Validation ── /// diff --git a/src/Test/Werkr.Tests.Data/packages.lock.json b/src/Test/Werkr.Tests.Data/packages.lock.json index 233f97e..4744dce 100644 --- a/src/Test/Werkr.Tests.Data/packages.lock.json +++ b/src/Test/Werkr.Tests.Data/packages.lock.json @@ -52,8 +52,8 @@ }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w==" + "resolved": "10.0.5", + "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A==" }, "Microsoft.CodeCoverage": { "type": "Transitive", @@ -67,36 +67,36 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", "SQLitePCLRaw.core": "2.1.11" } }, @@ -112,22 +112,22 @@ }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Caching.Memory": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Compliance.Abstractions": { @@ -141,46 +141,46 @@ }, "Microsoft.Extensions.Configuration": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==", + "resolved": "10.0.5", + "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ==" + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DependencyInjection.AutoActivation": { "type": "Transitive", @@ -192,26 +192,26 @@ }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==", + "resolved": "10.0.5", + "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { @@ -229,26 +229,26 @@ }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==", + "resolved": "10.0.5", + "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileSystemGlobbing": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg==" + "resolved": "10.0.5", + "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Http": { "type": "Transitive", @@ -274,12 +274,12 @@ }, "Microsoft.Extensions.Logging": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Configuration": { @@ -304,29 +304,29 @@ }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==", + "resolved": "10.0.5", + "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg==" + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.Extensions.Resilience": { "type": "Transitive", @@ -526,8 +526,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } @@ -668,8 +668,8 @@ "type": "Project", "dependencies": { "Grpc.AspNetCore": "[2.76.0, )", - "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.4, )", - "Microsoft.AspNetCore.OpenApi": "[10.0.4, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.5, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )", "Microsoft.IdentityModel.JsonWebTokens": "[8.16.0, )", "Serilog.AspNetCore": "[10.0.0, )", "Serilog.Sinks.Console": "[6.1.1, )", @@ -685,8 +685,8 @@ "type": "Project", "dependencies": { "Google.Protobuf": "[3.34.0, )", - "Microsoft.AspNetCore.Authorization": "[10.0.4, )", - "Microsoft.Extensions.Configuration.Json": "[10.0.4, )", + "Microsoft.AspNetCore.Authorization": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Json": "[10.0.5, )", "Microsoft.IdentityModel.Tokens": "[8.16.0, )", "Werkr.Common.Configuration": "[1.0.0, )" } @@ -698,8 +698,8 @@ "type": "Project", "dependencies": { "Grpc.Net.Client": "[2.76.0, )", - "Microsoft.Extensions.Hosting.Abstractions": "[10.0.4, )", - "System.Security.Cryptography.ProtectedData": "[10.0.4, )", + "Microsoft.Extensions.Hosting.Abstractions": "[10.0.5, )", + "System.Security.Cryptography.ProtectedData": "[10.0.5, )", "Werkr.Common": "[1.0.0, )", "Werkr.Data": "[1.0.0, )" } @@ -708,9 +708,9 @@ "type": "Project", "dependencies": { "EFCore.NamingConventions": "[10.0.1, )", - "Microsoft.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", "Werkr.Common": "[1.0.0, )" } }, @@ -771,104 +771,110 @@ "Microsoft.Extensions.Http": "8.0.0" } }, + "Grpc.Tools": { + "type": "CentralTransitive", + "requested": "[2.78.0, )", + "resolved": "2.78.0", + "contentHash": "6jPG2gHon+w2PczW8jjrCRnW/g9eEfCdd7aK6mDooptWtuPsV3ZxAwKKEx7LGEDVoT4c2SViRl8Yu3L1XiWIIg==" + }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1mUwGyeGrGETdhlU/ZzNZeN2J6ugqf9EztZyG+WQIZwkaH5+lFNza15+cNCXhXNA0MKo1iohr5vj60eqR2J44Q==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } }, "Microsoft.AspNetCore.Authorization": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==", "dependencies": { - "Microsoft.AspNetCore.Metadata": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" + "Microsoft.AspNetCore.Metadata": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.AspNetCore.OpenApi": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "OsEhbmT4Xenukau5YCR867gr/HmuAJ9DqMBPQGTcmdNU/TqBqdcnB+yLNwD/mTdkHzLBB+XG7cI4H1L5B1jx+Q==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==", "dependencies": { "Microsoft.OpenApi": "2.0.0" } }, "Microsoft.Data.Sqlite.Core": { "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", "dependencies": { "SQLitePCLRaw.core": "2.1.11" } }, "Microsoft.EntityFrameworkCore": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", "SQLitePCLRaw.core": "2.1.11" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "+5mQrqlBhqNUaPyDmFSNM/qiWStvE9LMxZW1MRF0NhEbO781xNeKryXNR9gGDJ0PmYFDAVoMT9ffX+I15LPFTw==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Http.Resilience": { @@ -884,11 +890,11 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.ServiceDiscovery": { @@ -922,13 +928,13 @@ }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" } }, "OpenTelemetry.Exporter.OpenTelemetryProtocol": { @@ -997,18 +1003,18 @@ "System.IdentityModel.Tokens.Jwt": { "type": "CentralTransitive", "requested": "[8.16.0, )", - "resolved": "8.0.1", - "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "resolved": "8.16.0", + "contentHash": "rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==", "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", - "Microsoft.IdentityModel.Tokens": "8.0.1" + "Microsoft.IdentityModel.JsonWebTokens": "8.16.0", + "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "System.Security.Cryptography.ProtectedData": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg==" } } } diff --git a/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs b/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs index c648a1a..7422096 100644 --- a/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs +++ b/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs @@ -45,7 +45,11 @@ public class PageAuthorizationTests { ["/tasks/create"] = string.Empty, ["/tasks/{Id:long}"] = string.Empty, ["/workflows"] = string.Empty, - ["/workflows/create"] = string.Empty, + ["/workflows/create"] = "Admin,Operator", + ["/workflows/new/dag-editor"] = "Admin,Operator", + ["/workflows/new/edit"] = "Admin,Operator", + ["/workflows/{Id:long}/dag-editor"] = "Admin,Operator", + ["/workflows/{Id:long}/edit"] = "Admin,Operator", ["/workflows/{Id:long}"] = string.Empty, ["/workflows/{WorkflowId:long}/runs"] = string.Empty, ["/workflows/runs/{RunId:guid}"] = string.Empty, diff --git a/src/Test/Werkr.Tests.Server/Components/ActionParameterEditorTests.cs b/src/Test/Werkr.Tests.Server/Components/ActionParameterEditorTests.cs index 01d9745..4bfff68 100644 --- a/src/Test/Werkr.Tests.Server/Components/ActionParameterEditorTests.cs +++ b/src/Test/Werkr.Tests.Server/Components/ActionParameterEditorTests.cs @@ -37,15 +37,15 @@ public void Renders_Optgroups_For_Each_Category( ) { } /// - /// Verifies that all 27 action options appear in the dropdown. + /// Verifies that all 31 action options appear in the dropdown. /// [TestMethod] - public void Renders_All_TwentySeven_Actions_In_Dropdown( ) { + public void Renders_All_ThirtyOne_Actions_In_Dropdown( ) { IRenderedComponent cut = Render( ); // All