From 1e6fd2e92c50d575c9b2dadec4bb40f0e9398a1e Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 21 Jul 2026 17:51:31 -0600 Subject: [PATCH 01/10] Add spec tests for ServiceControlQueueAddress on add instance Covers issue #4753, where SCMU does not set ServiceControl.Audit/ServiceControlQueueAddress when only adding an audit instance. The tests describe the expected behavior of ServiceControlAddViewModel: - Error + audit installed together: the queue address is the name of the error instance being installed, and no selection dropdown is shown. - Audit only with a single existing error instance: the address is auto-detected from it, no dropdown. - Audit only with multiple existing error instances: the dropdown is shown with all of them and Save is blocked until one is picked. - Audit only with no existing error instance: Save is blocked by a validation error. - Error instance only: no queue address validation error. --- .../ServiceControlQueueAddressTests.cs | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/ServiceControl.Config.Tests/AddInstance/ServiceControlQueueAddressTests.cs diff --git a/src/ServiceControl.Config.Tests/AddInstance/ServiceControlQueueAddressTests.cs b/src/ServiceControl.Config.Tests/AddInstance/ServiceControlQueueAddressTests.cs new file mode 100644 index 0000000000..68929e7acb --- /dev/null +++ b/src/ServiceControl.Config.Tests/AddInstance/ServiceControlQueueAddressTests.cs @@ -0,0 +1,176 @@ +namespace ServiceControl.Config.Tests.AddInstance +{ + using System.ComponentModel; + using NUnit.Framework; + using ServiceControl.Config.UI.InstanceAdd; + + /// + /// Spec for https://github.com/Particular/ServiceControl/issues/4753 + /// SCMU does not set ServiceControl.Audit/ServiceControlQueueAddress when only adding audit instances. + /// + /// Expected behavior: + /// - Installing error + audit together: queue address is the name of the error instance being installed. + /// - Installing audit only, one existing error instance on the machine: auto-detect and use its name. + /// - Installing audit only, multiple existing error instances: user must pick one from a dropdown + /// (dropdown only visible in this case); Save is blocked until a selection is made. + /// - Installing audit only, no existing error instance: Save is blocked by a validation error. + /// + public class ServiceControlQueueAddressTests + { + [Test] + public void Queue_address_is_the_new_error_instance_name_when_error_instance_is_installed_together() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = true, + InstallAuditInstance = true, + SubmitAttempted = true, + // No pre-existing error instances on the machine + GetInstalledErrorInstanceNames = () => new string[0] + }; + + viewModel.ErrorInstanceName = "My.Error.Instance"; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("My.Error.Instance")); + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + + [Test] + public void Queue_address_is_autodetected_when_adding_audit_only_and_a_single_error_instance_exists() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl" } + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("Particular.ServiceControl")); + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False, "Dropdown must not show when there is only one existing error instance"); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + + [Test] + public void Queue_address_dropdown_is_shown_only_when_adding_audit_only_and_multiple_error_instances_exist() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } + }; + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.True); + Assert.That(viewModel.ServiceControlQueueAddressOptions, Is.EquivalentTo(new[] + { + "Particular.ServiceControl", + "Particular.ServiceControl.2" + })); + } + } + + [Test] + public void Queue_address_dropdown_is_not_shown_when_error_instance_is_installed_together_even_if_multiple_error_instances_exist() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = true, + InstallAuditInstance = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } + }; + + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); + } + + [Test] + public void Save_is_blocked_when_adding_audit_only_and_multiple_error_instances_exist_until_one_is_selected() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + // No selection made yet: must be blocked + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Not.Empty, + "A validation error is expected until the user picks one of the existing error instances"); + + // User picks an instance from the dropdown + viewModel.ServiceControlQueueAddress = "Particular.ServiceControl.2"; + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("Particular.ServiceControl.2")); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + + [Test] + public void Save_is_blocked_when_adding_audit_only_and_no_error_instance_exists() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[0] + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.Null.Or.Empty); + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Not.Empty, + "A validation error is expected so the user cannot proceed without an existing error instance to connect to"); + } + } + + [Test] + public void No_queue_address_validation_error_when_only_installing_an_error_instance() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = true, + InstallAuditInstance = false, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[0] + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + + static INotifyDataErrorInfo GetNotifyErrorInfo(object vm) => vm as INotifyDataErrorInfo; + } +} From 1c6a6ed0e1251650914ff4b65c7c58b52c08b07d Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 22 Jul 2026 08:54:40 -0600 Subject: [PATCH 02/10] Add spec doc for audit instance queue address Replaces the initial ServiceControlQueueAddressTests with an executable specification organized as feature > rule > examples, mirroring the new docs/specs/audit-instance-servicecontrol-queue-address.md. The spec captures the four rules for bug #4753 (SCMU does not set ServiceControl.Audit/ServiceControlQueueAddress when only adding an audit instance): the instance installed in the same session always wins, auto-detect when exactly one error instance exists, require an explicit choice when several exist, and block Save when none exist. These are outer-loop TDD tests and reference members that do not exist yet: GetInstalledErrorInstanceNames, ServiceControlQueueAddress, ServiceControlQueueAddressOptions, ShowServiceControlQueueAddressSelection. --- ...t-instance-servicecontrol-queue-address.md | 70 +++++++ ...AuditInstanceServiceControlQueueAddress.cs | 196 ++++++++++++++++++ .../ServiceControlQueueAddressTests.cs | 176 ---------------- 3 files changed, 266 insertions(+), 176 deletions(-) create mode 100644 docs/specs/audit-instance-servicecontrol-queue-address.md create mode 100644 src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs delete mode 100644 src/ServiceControl.Config.Tests/AddInstance/ServiceControlQueueAddressTests.cs diff --git a/docs/specs/audit-instance-servicecontrol-queue-address.md b/docs/specs/audit-instance-servicecontrol-queue-address.md new file mode 100644 index 0000000000..78b680d0b0 --- /dev/null +++ b/docs/specs/audit-instance-servicecontrol-queue-address.md @@ -0,0 +1,70 @@ +# Feature: Audit Instance ServiceControl Queue Address + +**As an operator adding instances through ServiceControl Management Utility (SCMU), I want every audit instance to be connected to a ServiceControl (error) instance so that messages the audit instance sends to the error instance always have a valid destination.** + +> Bug: [#4753 — SCMU does not set ServiceControlQueueAddress when only adding audit instances](https://github.com/Particular/ServiceControl/issues/4753). +> When the `ServiceControl.Audit/ServiceControlQueueAddress` setting is missing from +> `ServiceControl.Audit.exe.config`, the audit instance fails at runtime with +> ["no destination specified for message"](https://docs.particular.net/servicecontrol/troubleshooting#no-destination-specified-for-message). + +## Rules and Examples + +### Rule 1: Must address the audit instance to the error instance installed in the same session + +When the user installs an error instance and an audit instance together, the audit +instance's queue address is the name of the error instance being installed — never an +already-installed one. + +- **Example:** The one where both instances are installed together and the audit + instance's queue address is the new error instance's name. +- **Counter-example:** The one where other error instances already exist on the + machine, yet no choice is offered — the error instance being installed always wins. + +--- + +### Rule 2: Should auto-detect the existing error instance when adding an audit instance alone + +- **Example:** The one where exactly one error instance exists on the machine and its + name is used as the queue address without any user input (no dropdown shown). + +--- + +### Rule 3: Must require an explicit choice when multiple existing error instances are found + +Auto-detection cannot guess between several error instances — picking one silently +risks routing messages to the wrong instance. The choice dropdown is shown **only** in +this case. + +- **Example:** The one where two error instances exist and the dropdown offers both. +- **Example:** The one where Save is blocked until the user picks one of the detected + instances, and unblocked once a choice is made. + +--- + +### Rule 4: Must block installation when no error instance exists to connect to + +An audit instance without a reachable error instance is misconfigured by definition; +SCMU must not produce it. + +- **Example:** The one where no error instance exists, the user adds an audit instance + alone, and a validation error prevents the installation from proceeding. +- **Counter-example:** The one where only an error instance is being installed — the + queue address does not apply and no validation error is raised. + +## Resolved decisions (for implementation) + +- **Auto-detect source:** installed Windows error instances, discovered via + `InstanceFinder.ServiceControlInstances()`; exposed on the view model through a + `GetInstalledErrorInstanceNames` function seam (mirrors the existing + `GetWindowsServiceNames` pattern) so tests can substitute it. +- **Multiple instances found:** user must choose from a dropdown that is visible only + when adding an audit instance alone **and** more than one error instance is detected. +- **No instance found:** Save is blocked by a validation error; deploying with + PowerShell remains the path for advanced scenarios. +- **Acceptance tier:** view model + validator observed through `INotifyDataErrorInfo` + — the same mechanism the UI uses to block Save. A full SCMU end-to-end test (install + a Windows service, inspect the written config file) is not automatable in this + repository's test suites. +- **Out of scope:** registering the new audit instance as a remote of the existing + error instance (`AddRemoteInstance` is only called when both instances are installed + together) — candidate for a follow-up issue. diff --git a/src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs b/src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs new file mode 100644 index 0000000000..cc2105f8ab --- /dev/null +++ b/src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs @@ -0,0 +1,196 @@ +namespace ServiceControl.Config.Tests.AddInstance +{ + using System.ComponentModel; + using NUnit.Framework; + using ServiceControl.Config.UI.InstanceAdd; + + /// + /// Executable specification for docs/specs/audit-instance-servicecontrol-queue-address.md + /// (bug https://github.com/Particular/ServiceControl/issues/4753). + /// + /// Organized as feature > rule > examples: + /// - this outer class is the feature, + /// - each nested fixture is one rule from the spec, + /// - each test is one example, named with the spec's "The one where ..." language. + /// + /// These tests are the OUTER loop of a double-loop TDD process. They observe the view + /// model and its validator through INotifyDataErrorInfo - the same mechanism the UI + /// uses to block Save - and reference members that do not exist yet: + /// GetInstalledErrorInstanceNames, ServiceControlQueueAddress, + /// ServiceControlQueueAddressOptions, ShowServiceControlQueueAddressSelection. + /// + public class AuditInstanceServiceControlQueueAddress + { + [TestFixture] + public class Rule_1_Must_address_the_audit_instance_to_the_error_instance_installed_in_the_same_session + { + [Test] + public void The_one_where_both_instances_are_installed_together_and_the_new_error_instance_name_is_used() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = true, + InstallAuditInstance = true, + SubmitAttempted = true, + // No pre-existing error instances on the machine + GetInstalledErrorInstanceNames = () => new string[0] + }; + + viewModel.ErrorInstanceName = "My.Error.Instance"; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("My.Error.Instance")); + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + + [Test] + public void The_one_where_other_error_instances_already_exist_yet_no_choice_is_offered_because_the_instance_being_installed_wins() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = true, + InstallAuditInstance = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } + }; + + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); + } + } + + [TestFixture] + public class Rule_2_Should_auto_detect_the_existing_error_instance_when_adding_an_audit_instance_alone + { + [Test] + public void The_one_where_a_single_error_instance_exists_and_its_name_is_used_without_any_user_input() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl" } + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("Particular.ServiceControl")); + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False, "Dropdown must not show when there is only one existing error instance"); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + } + + [TestFixture] + public class Rule_3_Must_require_an_explicit_choice_when_multiple_existing_error_instances_are_found + { + [Test] + public void The_one_where_two_error_instances_exist_and_the_dropdown_offers_both() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } + }; + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.True); + Assert.That(viewModel.ServiceControlQueueAddressOptions, Is.EquivalentTo(new[] + { + "Particular.ServiceControl", + "Particular.ServiceControl.2" + })); + } + } + + [Test] + public void The_one_where_save_is_blocked_until_the_user_picks_one_of_the_detected_instances() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + // No selection made yet: must be blocked + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Not.Empty, + "A validation error is expected until the user picks one of the existing error instances"); + + // User picks an instance from the dropdown + viewModel.ServiceControlQueueAddress = "Particular.ServiceControl.2"; + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("Particular.ServiceControl.2")); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + } + + [TestFixture] + public class Rule_4_Must_block_installation_when_no_error_instance_exists_to_connect_to + { + [Test] + public void The_one_where_no_error_instance_exists_and_a_validation_error_prevents_the_installation_from_proceeding() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[0] + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.Null.Or.Empty); + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Not.Empty, + "A validation error is expected so the user cannot proceed without an existing error instance to connect to"); + } + } + + [Test] + public void The_one_where_only_an_error_instance_is_installed_and_the_queue_address_does_not_apply() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = true, + InstallAuditInstance = false, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[0] + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + + static INotifyDataErrorInfo GetNotifyErrorInfo(object vm) => vm as INotifyDataErrorInfo; + } +} diff --git a/src/ServiceControl.Config.Tests/AddInstance/ServiceControlQueueAddressTests.cs b/src/ServiceControl.Config.Tests/AddInstance/ServiceControlQueueAddressTests.cs deleted file mode 100644 index 68929e7acb..0000000000 --- a/src/ServiceControl.Config.Tests/AddInstance/ServiceControlQueueAddressTests.cs +++ /dev/null @@ -1,176 +0,0 @@ -namespace ServiceControl.Config.Tests.AddInstance -{ - using System.ComponentModel; - using NUnit.Framework; - using ServiceControl.Config.UI.InstanceAdd; - - /// - /// Spec for https://github.com/Particular/ServiceControl/issues/4753 - /// SCMU does not set ServiceControl.Audit/ServiceControlQueueAddress when only adding audit instances. - /// - /// Expected behavior: - /// - Installing error + audit together: queue address is the name of the error instance being installed. - /// - Installing audit only, one existing error instance on the machine: auto-detect and use its name. - /// - Installing audit only, multiple existing error instances: user must pick one from a dropdown - /// (dropdown only visible in this case); Save is blocked until a selection is made. - /// - Installing audit only, no existing error instance: Save is blocked by a validation error. - /// - public class ServiceControlQueueAddressTests - { - [Test] - public void Queue_address_is_the_new_error_instance_name_when_error_instance_is_installed_together() - { - var viewModel = new ServiceControlAddViewModel - { - InstallErrorInstance = true, - InstallAuditInstance = true, - SubmitAttempted = true, - // No pre-existing error instances on the machine - GetInstalledErrorInstanceNames = () => new string[0] - }; - - viewModel.ErrorInstanceName = "My.Error.Instance"; - - viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); - - var notifyErrorInfo = GetNotifyErrorInfo(viewModel); - - using (Assert.EnterMultipleScope()) - { - Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("My.Error.Instance")); - Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); - Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); - } - } - - [Test] - public void Queue_address_is_autodetected_when_adding_audit_only_and_a_single_error_instance_exists() - { - var viewModel = new ServiceControlAddViewModel - { - InstallErrorInstance = false, - InstallAuditInstance = true, - SubmitAttempted = true, - GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl" } - }; - - viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); - - var notifyErrorInfo = GetNotifyErrorInfo(viewModel); - - using (Assert.EnterMultipleScope()) - { - Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("Particular.ServiceControl")); - Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False, "Dropdown must not show when there is only one existing error instance"); - Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); - } - } - - [Test] - public void Queue_address_dropdown_is_shown_only_when_adding_audit_only_and_multiple_error_instances_exist() - { - var viewModel = new ServiceControlAddViewModel - { - InstallErrorInstance = false, - InstallAuditInstance = true, - GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } - }; - - using (Assert.EnterMultipleScope()) - { - Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.True); - Assert.That(viewModel.ServiceControlQueueAddressOptions, Is.EquivalentTo(new[] - { - "Particular.ServiceControl", - "Particular.ServiceControl.2" - })); - } - } - - [Test] - public void Queue_address_dropdown_is_not_shown_when_error_instance_is_installed_together_even_if_multiple_error_instances_exist() - { - var viewModel = new ServiceControlAddViewModel - { - InstallErrorInstance = true, - InstallAuditInstance = true, - GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } - }; - - Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); - } - - [Test] - public void Save_is_blocked_when_adding_audit_only_and_multiple_error_instances_exist_until_one_is_selected() - { - var viewModel = new ServiceControlAddViewModel - { - InstallErrorInstance = false, - InstallAuditInstance = true, - SubmitAttempted = true, - GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } - }; - - viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); - - var notifyErrorInfo = GetNotifyErrorInfo(viewModel); - - // No selection made yet: must be blocked - Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Not.Empty, - "A validation error is expected until the user picks one of the existing error instances"); - - // User picks an instance from the dropdown - viewModel.ServiceControlQueueAddress = "Particular.ServiceControl.2"; - - using (Assert.EnterMultipleScope()) - { - Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("Particular.ServiceControl.2")); - Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); - } - } - - [Test] - public void Save_is_blocked_when_adding_audit_only_and_no_error_instance_exists() - { - var viewModel = new ServiceControlAddViewModel - { - InstallErrorInstance = false, - InstallAuditInstance = true, - SubmitAttempted = true, - GetInstalledErrorInstanceNames = () => new string[0] - }; - - viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); - - var notifyErrorInfo = GetNotifyErrorInfo(viewModel); - - using (Assert.EnterMultipleScope()) - { - Assert.That(viewModel.ServiceControlQueueAddress, Is.Null.Or.Empty); - Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); - Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Not.Empty, - "A validation error is expected so the user cannot proceed without an existing error instance to connect to"); - } - } - - [Test] - public void No_queue_address_validation_error_when_only_installing_an_error_instance() - { - var viewModel = new ServiceControlAddViewModel - { - InstallErrorInstance = true, - InstallAuditInstance = false, - SubmitAttempted = true, - GetInstalledErrorInstanceNames = () => new string[0] - }; - - viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); - - var notifyErrorInfo = GetNotifyErrorInfo(viewModel); - - Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); - } - - static INotifyDataErrorInfo GetNotifyErrorInfo(object vm) => vm as INotifyDataErrorInfo; - } -} From 6b6d37b740602b17b0b876933b1b83bd8e33fc42 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 22 Jul 2026 12:48:33 -0700 Subject: [PATCH 03/10] Rule 1: address audit instance to the error instance installed in the same session ServiceControlQueueAddress resolves to ErrorInstanceName when an error instance is being installed. Adds the GetInstalledErrorInstanceNames seam (mirrors GetWindowsServiceNames) and naive stubs for the members Rules 2-4 will drive. Suppresses IDE0017 in the test project: executable specs assign properties after construction to mirror user interaction order. --- src/ServiceControl.Config.Tests/.editorconfig | 4 ++++ .../InstanceAdd/ServiceControlAddViewModel.cs | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/ServiceControl.Config.Tests/.editorconfig b/src/ServiceControl.Config.Tests/.editorconfig index 5f68a610b3..c5410d8c02 100644 --- a/src/ServiceControl.Config.Tests/.editorconfig +++ b/src/ServiceControl.Config.Tests/.editorconfig @@ -2,3 +2,7 @@ # Justification: Test project dotnet_diagnostic.CA2007.severity = none + +# Justification: Executable specifications intentionally assign properties after the +# object initializer to mirror user interaction order (e.g. typing a name after load) +dotnet_diagnostic.IDE0017.severity = none diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs index cc8bdb3d2a..d936c5210f 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.Config.UI.InstanceAdd +namespace ServiceControl.Config.UI.InstanceAdd { using System; using System.Collections.Generic; @@ -8,6 +8,7 @@ using System.Windows.Input; using PropertyChanged; using ServiceControl.Config.Extensions; + using ServiceControlInstaller.Engine.Instances; using Validar; using Xaml.Controls; @@ -18,6 +19,7 @@ public ServiceControlAddViewModel() { DisplayName = "ADD SERVICECONTROL"; GetWindowsServiceNames = () => ServiceController.GetServices().Select(windowsService => windowsService.ServiceName).ToArray(); + GetInstalledErrorInstanceNames = () => InstanceFinder.ServiceControlInstances().Select(instance => instance.Name).ToArray(); ConventionName = "Particular.ServiceControl"; OnConventionNameChanged(); @@ -47,6 +49,20 @@ void ServiceControl_PropertyChanged(object sender, System.ComponentModel.Propert public Func GetWindowsServiceNames { get; set; } + public Func GetInstalledErrorInstanceNames { get; set; } + + public string[] ServiceControlQueueAddressOptions => null; + + public string ServiceControlQueueAddress + { + get => InstallErrorInstance ? ErrorInstanceName : serviceControlQueueAddress; + set => serviceControlQueueAddress = value; + } + + string serviceControlQueueAddress; + + public bool ShowServiceControlQueueAddressSelection => false; + public string ConventionName { get; set; } public void OnConventionNameChanged() From 52942fd9c8b6393f1c5bc76b15a0836043331c24 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 22 Jul 2026 12:50:01 -0700 Subject: [PATCH 04/10] Rule 2: auto-detect the existing error instance when adding an audit instance alone When no error instance is being installed and exactly one is detected on the machine, its name is used as the queue address without user input. --- .../UI/InstanceAdd/ServiceControlAddViewModel.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs index d936c5210f..2b22f70596 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs @@ -55,7 +55,19 @@ void ServiceControl_PropertyChanged(object sender, System.ComponentModel.Propert public string ServiceControlQueueAddress { - get => InstallErrorInstance ? ErrorInstanceName : serviceControlQueueAddress; + get + { + if (InstallErrorInstance) + { + return ErrorInstanceName; + } + + var installedErrorInstanceNames = GetInstalledErrorInstanceNames(); + + return installedErrorInstanceNames.Length == 1 + ? installedErrorInstanceNames[0] + : serviceControlQueueAddress; + } set => serviceControlQueueAddress = value; } From f8b08777bf30b911b5902481b5d9b664b50cc48d Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 22 Jul 2026 12:51:33 -0700 Subject: [PATCH 05/10] Rules 3-4: require explicit choice among multiple error instances, block when none exist The dropdown options and its visibility (audit-only + 2 or more detected) come from the installed-instance seam. A NotEmpty validation rule on ServiceControlQueueAddress blocks Save for audit-only adds until an address resolves - covering both the multiple-instances case (until the user picks) and the none-found case. Error-only installs are exempt. --- .../UI/InstanceAdd/ServiceControlAddViewModel.cs | 7 +++++-- .../UI/InstanceAdd/ServiceControlAddViewModelValidator.cs | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs index 2b22f70596..c6a53f470e 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs @@ -51,7 +51,7 @@ void ServiceControl_PropertyChanged(object sender, System.ComponentModel.Propert public Func GetInstalledErrorInstanceNames { get; set; } - public string[] ServiceControlQueueAddressOptions => null; + public string[] ServiceControlQueueAddressOptions => GetInstalledErrorInstanceNames(); public string ServiceControlQueueAddress { @@ -73,7 +73,10 @@ public string ServiceControlQueueAddress string serviceControlQueueAddress; - public bool ShowServiceControlQueueAddressSelection => false; + public bool ShowServiceControlQueueAddressSelection => + InstallAuditInstance + && !InstallErrorInstance + && GetInstalledErrorInstanceNames().Length > 1; public string ConventionName { get; set; } diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs index 7a627d954a..41aac63ee0 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs @@ -150,6 +150,11 @@ public ServiceControlAddViewModelValidator() .WithMessage("Audit instance name is already in use") .When(viewModel => viewModel.InstallAuditInstance); + RuleFor(viewModel => viewModel.ServiceControlQueueAddress) + .NotEmpty() + .WithMessage("An existing error instance must be selected to receive audit data") + .When(viewModel => viewModel.InstallAuditInstance && !viewModel.InstallErrorInstance); + RuleFor(x => x.AuditServiceAccount) .NotEmpty() .When(x => x.InstallAuditInstance && x.SubmitAttempted); From a6a1453926c63b4e2a6d9757df28e4e9ad8fb536 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 22 Jul 2026 12:53:09 -0700 Subject: [PATCH 06/10] Wire resolved queue address into the installer and add the selection UI The attachment writes viewModel.ServiceControlQueueAddress instead of the null-conditional expression that produced an empty string for audit-only adds - the root cause of #4753, since empty values are silently skipped when writing the config file. The audit section gets a ComboBox (visible only when multiple error instances are detected) and a warning for the none-found case. Explicit change notifications are raised for the derived computed properties because the install checkboxes live on the base class, outside Fody's dependency analysis. --- .../InstanceAdd/ServiceControlAddAttachment.cs | 2 +- .../UI/InstanceAdd/ServiceControlAddView.xaml | 15 +++++++++++++++ .../InstanceAdd/ServiceControlAddViewModel.cs | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs index 1d3c08009d..5c9863dd03 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs @@ -100,7 +100,7 @@ async Task Add() auditNewInstance.AuditRetentionPeriod = viewModel.ServiceControlAudit.AuditRetentionPeriod; auditNewInstance.ServiceAccount = viewModel.ServiceControlAudit.ServiceAccount; auditNewInstance.ServiceAccountPwd = viewModel.ServiceControlAudit.Password; - auditNewInstance.ServiceControlQueueAddress = serviceControlNewInstance == null ? string.Empty : serviceControlNewInstance.InstanceName; + auditNewInstance.ServiceControlQueueAddress = viewModel.ServiceControlQueueAddress; auditNewInstance.EnableFullTextSearchOnBodies = viewModel.ServiceControlAudit.EnableFullTextSearchOnBodies.Value; } diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml index 9ace3fbd7a..a1d1739098 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml @@ -299,6 +299,21 @@ + + + + diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs index c6a53f470e..5a4bf21aa4 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs @@ -32,6 +32,18 @@ public ServiceControlAddViewModel() ServiceControl.PropertyChanged += ServiceControl_PropertyChanged; ServiceControlAudit.PropertyChanged += ServiceControl_PropertyChanged; + PropertyChanged += (_, e) => + { + // InstallErrorInstance/InstallAuditInstance live on the base class, so Fody + // cannot infer that these derived computed properties depend on them + if (e.PropertyName is nameof(InstallErrorInstance) or nameof(InstallAuditInstance)) + { + NotifyOfPropertyChange(nameof(ServiceControlQueueAddress)); + NotifyOfPropertyChange(nameof(ServiceControlQueueAddressOptions)); + NotifyOfPropertyChange(nameof(ShowServiceControlQueueAddressSelection)); + NotifyOfPropertyChange(nameof(ShowNoErrorInstanceFoundWarning)); + } + }; } void ServiceControl_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -78,6 +90,11 @@ public string ServiceControlQueueAddress && !InstallErrorInstance && GetInstalledErrorInstanceNames().Length > 1; + public bool ShowNoErrorInstanceFoundWarning => + InstallAuditInstance + && !InstallErrorInstance + && GetInstalledErrorInstanceNames().Length == 0; + public string ConventionName { get; set; } public void OnConventionNameChanged() From c3e9bd9a2b8930fe8891425ab11d872d48e3132e Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 22 Jul 2026 13:02:23 -0700 Subject: [PATCH 07/10] Make construction-time unique-naming injectable for testability The constructor runs the name-uniquing convention against the machine's real Windows services before tests can substitute the GetWindowsServiceNames property seam, so screen-loaded tests asserting convention-derived paths failed on any machine with actual ServiceControl instances installed. An optional constructor parameter lets those tests inject a fake service list; Autofac supplies the default (real) implementation in production since the parameter is unresolvable and optional. --- .../ServiceControlAddScreenLoadedTests.cs | 6 +++--- .../UI/InstanceAdd/ServiceControlAddViewModel.cs | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/ServiceControl.Config.Tests/ServiceControlAddScreenLoadedTests.cs b/src/ServiceControl.Config.Tests/ServiceControlAddScreenLoadedTests.cs index 118d0bc687..3c97a982ec 100644 --- a/src/ServiceControl.Config.Tests/ServiceControlAddScreenLoadedTests.cs +++ b/src/ServiceControl.Config.Tests/ServiceControlAddScreenLoadedTests.cs @@ -153,7 +153,7 @@ public void Database_maintenance_port_number_are_set_to_defaults_with_no_validat [Test] public void Destination_path_is_null() { - var viewModel = new ServiceControlAddViewModel(); + var viewModel = new ServiceControlAddViewModel(() => []); var errorInfo = (INotifyDataErrorInfo)viewModel; @@ -169,7 +169,7 @@ public void Destination_path_is_null() [Test] public void Log_path_is_null() { - var viewModel = new ServiceControlAddViewModel(); + var viewModel = new ServiceControlAddViewModel(() => []); var errorInfo = (INotifyDataErrorInfo)viewModel; @@ -186,7 +186,7 @@ public void Log_path_is_null() [Test] public void Database_path_is_null() { - var viewModel = new ServiceControlAddViewModel(); + var viewModel = new ServiceControlAddViewModel(() => []); var errorInfo = (INotifyDataErrorInfo)viewModel; diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs index 5a4bf21aa4..ce26263a1b 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs @@ -15,10 +15,13 @@ namespace ServiceControl.Config.UI.InstanceAdd [InjectValidation] public class ServiceControlAddViewModel : ServiceControlEditorViewModel { - public ServiceControlAddViewModel() + // The constructor runs the unique-name convention against the machine's Windows + // services, so tests inject a fake here to stay environment-independent; the + // GetWindowsServiceNames property seam is set too late for construction-time logic + public ServiceControlAddViewModel(Func getWindowsServiceNames = null) { DisplayName = "ADD SERVICECONTROL"; - GetWindowsServiceNames = () => ServiceController.GetServices().Select(windowsService => windowsService.ServiceName).ToArray(); + GetWindowsServiceNames = getWindowsServiceNames ?? (() => ServiceController.GetServices().Select(windowsService => windowsService.ServiceName).ToArray()); GetInstalledErrorInstanceNames = () => InstanceFinder.ServiceControlInstances().Select(instance => instance.Name).ToArray(); ConventionName = "Particular.ServiceControl"; OnConventionNameChanged(); From e90eb0b0fb65321fa926633586e64bf804b3ac0f Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 23 Jul 2026 21:07:05 -0700 Subject: [PATCH 08/10] Simplify and clarify audit-instance error-instance selection UI copy Shorten the field label to "ERROR INSTANCE" and align the missing- instance warning and validation message wording for consistency. --- .../UI/InstanceAdd/ServiceControlAddView.xaml | 4 ++-- .../UI/InstanceAdd/ServiceControlAddViewModelValidator.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml index a1d1739098..58c8a3efed 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml @@ -300,7 +300,7 @@ diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs index 41aac63ee0..e60e132b7e 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs @@ -152,7 +152,7 @@ public ServiceControlAddViewModelValidator() RuleFor(viewModel => viewModel.ServiceControlQueueAddress) .NotEmpty() - .WithMessage("An existing error instance must be selected to receive audit data") + .WithMessage("An existing error instance must be selected for the audit instance to send messages to") .When(viewModel => viewModel.InstallAuditInstance && !viewModel.InstallErrorInstance); RuleFor(x => x.AuditServiceAccount) From 3d6e4fed7e5553e4e989aeb211bf3829a97e3e29 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 23 Jul 2026 21:08:34 -0700 Subject: [PATCH 09/10] Add manual test plan for audit-instance ServiceControl queue address Documents end-to-end scenarios for validating the "Add ServiceControl" screen rules on a real Windows machine, covering the service installation and config setting that automated tests can't verify. --- ...econtrol-queue-address-manual-test-plan.md | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md diff --git a/docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md b/docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md new file mode 100644 index 0000000000..fa2ed7baa4 --- /dev/null +++ b/docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md @@ -0,0 +1,277 @@ +# Manual Test Plan: Audit Instance ServiceControl Queue Address (SCMU) + +**Spec:** [audit-instance-servicecontrol-queue-address.md](audit-instance-servicecontrol-queue-address.md) +**Bug:** [#4753 — SCMU does not set ServiceControlQueueAddress when only adding audit instances](https://github.com/Particular/ServiceControl/issues/4753) + +This plan validates, end-to-end on a real machine, the rules implemented in the SCMU +"Add ServiceControl" screen. The automated acceptance tests cover the view model and +validator; this plan covers what those tests cannot: the actual Windows service +installation and the setting written to `ServiceControl.Audit.exe.config`. + +Scenarios are independent of each other and can be executed together (mob-style), +split among testers, or run async. Each scenario states its own preconditions, so any +tester can bring a machine into the required state without running previous scenarios. + +--- + +## Shared machine setup (once per test machine) + +All scenarios need this baseline. If you split scenarios among testers on different +machines, each machine needs this setup. + +1. **Windows machine or VM** with local administrator rights (SCMU requires elevation + to install Windows services). +2. **Build and launch the SCMU under test** from this branch: + - Build `ServiceControl.Config` (or use the packaged installer produced from the branch). + - Run it elevated. +3. **A working transport** available to the machine, so instances can actually be + installed and started. Any supported transport works; suggested low-friction options: + - RabbitMQ in a local Docker container, or + - SQL Server / PostgreSQL transport against a local instance. + Keep the connection string at hand; every scenario that installs an instance needs it. +4. **Snapshot/cleanup strategy:** scenarios require specific numbers of pre-installed + error instances (0, 1, or 2). Between scenarios, remove instances via SCMU + (instance → Remove, tick "delete database" and "delete logs") or restore a VM + snapshot of the clean baseline. Removal order does not matter for this plan. + +### How to verify the written setting (used by several scenarios) + +1. Open the audit instance's install path (visible on the instance card in SCMU), + e.g. `C:\ProgramData\Particular\ServiceControl\\` or the destination + path chosen during install. +2. Open `ServiceControl.Audit.exe.config`. +3. Locate ``. +4. The `value` must be exactly the expected error instance name for the scenario. +5. Additionally verify the audit instance **starts and stays running** (SCMU shows it + running; the instance log contains no + ["no destination specified for message"](https://docs.particular.net/servicecontrol/troubleshooting#no-destination-specified-for-message) + errors). + +### Terminology + +- **Error instance** = a "ServiceControl" instance (the SCMU add screen offers + "ServiceControl" and "ServiceControl Audit"; the first is referred to as the error + instance in the UI validation messages). +- **Add screen** = SCMU → New → "Add ServiceControl and Audit Instances". + +--- + +## Scenario matrix + +| # | Rule | Scenario | Pre-installed error instances required | +|---|------|----------|----------------------------------------| +| 1 | Rule 1 | Install both together → audit points at the new error instance | 0 | +| 2 | Rule 1 (counter) | Install both together with existing error instances → new error instance still wins, no dropdown | 2 | +| 3 | Rule 2 | Add audit alone, one existing error instance → auto-detected, no dropdown | 1 | +| 4 | Rule 3 | Add audit alone, two existing error instances → dropdown offers both | 2 | +| 5 | Rule 3 | Save blocked until a dropdown choice is made, unblocked after | 2 | +| 6 | Rule 4 | Add audit alone, no error instance → warning shown, install blocked | 0 | +| 7 | Rule 4 (counter) | Add error instance alone → no queue-address validation raised | 0 | + +Suggested split for three testers: A = 1, 2 (both-together flows); B = 3, 6, 7 +(auto-detect and blocking); C = 4, 5 (multiple-instance dropdown). Scenarios 2, 4 and 5 +share the same precondition (two error instances installed), so grouping them on one +machine saves setup time. + +--- + +## Scenario 1 — Rule 1: both instances installed together + +**Rule:** Must address the audit instance to the error instance installed in the same session. + +**Preconditions** +- No ServiceControl (error) or audit instances installed on the machine. +- Verify: SCMU instance list is empty. + +**Steps** +1. Open the Add screen. +2. Keep **both** checkboxes ticked (ServiceControl and ServiceControl Audit). +3. Note the error instance name (default `Particular.ServiceControl`). +4. Fill in transport, connection string, ports, and paths as needed. +5. Observe the audit section: no "ERROR INSTANCE" dropdown and no red + "No error instance was found..." warning should be visible. +6. Click Add and let the installation finish. + +**Pass criteria** +- [ ] No dropdown and no warning were shown in the audit section. +- [ ] Installation completes; both instances appear in SCMU and start. +- [ ] `ServiceControl.Audit.exe.config` contains + `ServiceControl.Audit/ServiceControlQueueAddress` = the error instance name from step 3. +- [ ] Audit instance runs with no "no destination specified for message" log errors. + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 2 — Rule 1 counter-example: existing instances do not win over the one being installed + +**Rule:** When installing both together, the error instance being installed always wins — never an already-installed one. + +**Preconditions** +- **Two** error instances already installed (e.g. `Particular.ServiceControl.A` and + `Particular.ServiceControl.B`), no audit instances. +- Tip: install them one at a time via the Add screen with only the ServiceControl + checkbox ticked, using distinct names/ports. +- Verify: SCMU lists exactly two error instances. + +**Steps** +1. Open the Add screen. +2. Keep both checkboxes ticked. +3. Give the new error instance a distinct name (e.g. `Particular.ServiceControl.New`). +4. Observe the audit section: even though two error instances exist, **no** + "ERROR INSTANCE" dropdown should be shown (the instance being installed wins). +5. Fill in the remaining fields and click Add. + +**Pass criteria** +- [ ] No dropdown was shown while both checkboxes were ticked. +- [ ] `ServiceControl.Audit.exe.config` contains + `ServiceControlQueueAddress` = `Particular.ServiceControl.New` (the new instance, + not `...A` or `...B`). +- [ ] Audit instance starts and stays running. + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 3 — Rule 2: auto-detect single existing error instance + +**Rule:** Should auto-detect the existing error instance when adding an audit instance alone. + +**Preconditions** +- Exactly **one** error instance installed (note its name, e.g. `Particular.ServiceControl`), + no audit instances. +- Verify: SCMU lists exactly one error instance. + +**Steps** +1. Open the Add screen. +2. **Untick** the ServiceControl checkbox; keep only ServiceControl Audit ticked. +3. Observe the audit section: no "ERROR INSTANCE" dropdown and no warning should be + visible — detection is silent. +4. Fill in the remaining fields and click Add. + +**Pass criteria** +- [ ] No dropdown and no warning were shown; Save was not blocked by the queue address. +- [ ] `ServiceControl.Audit.exe.config` contains + `ServiceControlQueueAddress` = the name of the pre-existing error instance. +- [ ] Audit instance starts and stays running. + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 4 — Rule 3: dropdown offers all detected error instances + +**Rule:** Must require an explicit choice when multiple existing error instances are found. + +**Preconditions** +- **Two** error instances installed (e.g. `Particular.ServiceControl.A`, + `Particular.ServiceControl.B`), no audit instances. +- Verify: SCMU lists exactly two error instances. + +**Steps** +1. Open the Add screen. +2. Untick the ServiceControl checkbox; keep only ServiceControl Audit ticked. +3. Locate the "ERROR INSTANCE" dropdown in the audit GENERAL section. +4. Open the dropdown and inspect its items. + +**Pass criteria** +- [ ] The dropdown is visible (only in this configuration — see also scenarios 1-3 + where it must be hidden). +- [ ] The dropdown contains exactly the two installed instance names, no more, no less. +- [ ] No instance is silently pre-selected on first open (no guess is made for the user). + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 5 — Rule 3: Save blocked until a choice is made + +**Rule:** Save is blocked until the user picks one of the detected instances, and unblocked once a choice is made. + +**Preconditions** +- Same as Scenario 4 (two error instances, no audit instances). Can be run in the same + session as Scenario 4. + +**Steps** +1. Open the Add screen; untick ServiceControl, keep ServiceControl Audit ticked. +2. Fill in **all other** required audit fields (name, transport, connection string, + ports, paths, service account) but leave the "ERROR INSTANCE" dropdown unselected. +3. Click Add. +4. Expect a validation error on the dropdown; the installation must not start. +5. Select one of the two instances in the dropdown (note which one, e.g. + `Particular.ServiceControl.B`). +6. Click Add again and let the installation finish. + +**Pass criteria** +- [ ] Step 3: install did not proceed; the validation error + "An existing error instance must be selected for the audit instance to send messages to" + was raised against the dropdown. +- [ ] Step 6: install proceeded after choosing. +- [ ] `ServiceControl.Audit.exe.config` contains `ServiceControlQueueAddress` = the + instance chosen in step 5. +- [ ] Audit instance starts and stays running. + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 6 — Rule 4: install blocked when no error instance exists + +**Rule:** Must block installation when no error instance exists to connect to. + +**Preconditions** +- **No** error instances installed (audit instances, if any, removed as well for a + clean read of the instance list). +- Verify: SCMU instance list contains no error instances. + +**Steps** +1. Open the Add screen. +2. Untick the ServiceControl checkbox; keep only ServiceControl Audit ticked. +3. Observe the audit GENERAL section. +4. Fill in all required audit fields and click Add. + +**Pass criteria** +- [ ] The red warning is shown: "No error instance was found on this machine. The + audit instance needs one to send messages to. Also install the error instance + above, or add one first." +- [ ] No "ERROR INSTANCE" dropdown is shown (there is nothing to choose from). +- [ ] Clicking Add does not install anything; a validation error blocks the save. +- [ ] Bonus check: ticking the ServiceControl checkbox again makes the warning + disappear (the error instance in the same session satisfies the requirement). + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 7 — Rule 4 counter-example: error-instance-only install is unaffected + +**Rule:** When only an error instance is being installed, the queue address does not apply and no validation error is raised. + +**Preconditions** +- No instances installed. +- Verify: SCMU instance list is empty. + +**Steps** +1. Open the Add screen. +2. Untick the ServiceControl **Audit** checkbox; keep only ServiceControl ticked. +3. Fill in the error instance fields and click Add. + +**Pass criteria** +- [ ] No queue-address validation error appears anywhere in the flow. +- [ ] The error instance installs and starts normally. + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Wrap-up + +- Collect results per scenario (pass/fail, tester, notes) in this file or a copy. +- Any failure: capture a screenshot of the Add screen, the SCMU log, and the + `ServiceControl.Audit.exe.config` (if written), and link them in the notes. +- Known out-of-scope behavior (do **not** log as failures): + - The new audit instance is *not* registered as a remote of a pre-existing error + instance (`AddRemoteInstance` only runs when both are installed together) — + tracked as a follow-up candidate in the spec. + - PowerShell (`New-ServiceControlAuditInstance`) already enforces the parameter as + mandatory and is not covered by this plan. From cda9f1a7663734749041f9067abf1dde4b91e38e Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 24 Jul 2026 07:00:00 -0700 Subject: [PATCH 10/10] Mark spec docs as PR-scoped working artifacts to be deleted Add a warning banner to both spec documents flagging them as temporary working artifacts for this PR, and drop the now-stale comment references in the test file pointing at those docs. --- ...rvicecontrol-queue-address-manual-test-plan.md | 5 +++++ ...audit-instance-servicecontrol-queue-address.md | 5 +++++ .../AuditInstanceServiceControlQueueAddress.cs | 15 +++++++-------- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md b/docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md index fa2ed7baa4..3988f8b48f 100644 --- a/docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md +++ b/docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md @@ -1,5 +1,10 @@ # Manual Test Plan: Audit Instance ServiceControl Queue Address (SCMU) +> [!WARNING] +> **This document is a working artifact for the manual validation of this feature and +> is meant to be DELETED before merging the PR.** Record the test results here (or in +> a copy) while the PR is open. + **Spec:** [audit-instance-servicecontrol-queue-address.md](audit-instance-servicecontrol-queue-address.md) **Bug:** [#4753 — SCMU does not set ServiceControlQueueAddress when only adding audit instances](https://github.com/Particular/ServiceControl/issues/4753) diff --git a/docs/specs/audit-instance-servicecontrol-queue-address.md b/docs/specs/audit-instance-servicecontrol-queue-address.md index 78b680d0b0..e8841e53de 100644 --- a/docs/specs/audit-instance-servicecontrol-queue-address.md +++ b/docs/specs/audit-instance-servicecontrol-queue-address.md @@ -1,5 +1,10 @@ # Feature: Audit Instance ServiceControl Queue Address +> [!WARNING] +> **This document is a working artifact for the development of this feature and is +> meant to be DELETED before merging the PR.** Do not reference it from code or docs +> that will outlive the PR. + **As an operator adding instances through ServiceControl Management Utility (SCMU), I want every audit instance to be connected to a ServiceControl (error) instance so that messages the audit instance sends to the error instance always have a valid destination.** > Bug: [#4753 — SCMU does not set ServiceControlQueueAddress when only adding audit instances](https://github.com/Particular/ServiceControl/issues/4753). diff --git a/src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs b/src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs index cc2105f8ab..bfd43554ac 100644 --- a/src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs +++ b/src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs @@ -5,19 +5,18 @@ namespace ServiceControl.Config.Tests.AddInstance using ServiceControl.Config.UI.InstanceAdd; /// - /// Executable specification for docs/specs/audit-instance-servicecontrol-queue-address.md + /// Executable specification for the audit instance ServiceControl queue address feature /// (bug https://github.com/Particular/ServiceControl/issues/4753). /// /// Organized as feature > rule > examples: /// - this outer class is the feature, - /// - each nested fixture is one rule from the spec, - /// - each test is one example, named with the spec's "The one where ..." language. + /// - each nested fixture is one rule, + /// - each test is one example, named with "The one where ..." language. /// - /// These tests are the OUTER loop of a double-loop TDD process. They observe the view - /// model and its validator through INotifyDataErrorInfo - the same mechanism the UI - /// uses to block Save - and reference members that do not exist yet: - /// GetInstalledErrorInstanceNames, ServiceControlQueueAddress, - /// ServiceControlQueueAddressOptions, ShowServiceControlQueueAddressSelection. + /// The tests observe the view model and its validator through INotifyDataErrorInfo - + /// the same mechanism the UI uses to block Save - and substitute the + /// GetInstalledErrorInstanceNames seam to simulate the error instances installed on + /// the machine. /// public class AuditInstanceServiceControlQueueAddress {