From de775048f034aba70548b783e7ace24c5321934f Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 11 Sep 2026 16:18:17 +0200 Subject: [PATCH] Added a NUnit test project for the whole solution --- AGENTS.md | 16 +++++++- app/MindWork AI Studio.sln | 6 +++ .../MindWork AI Studio.csproj | 9 +++++ app/Tests/Models/TestHarnessTests.cs | 39 ++++++++++++++++++ app/Tests/TestHost.cs | 26 ++++++++++++ app/Tests/Tests.csproj | 40 +++++++++++++++++++ 6 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 app/Tests/Models/TestHarnessTests.cs create mode 100644 app/Tests/TestHost.cs create mode 100644 app/Tests/Tests.csproj diff --git a/AGENTS.md b/AGENTS.md index bb70bb72..4415b6e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,7 +80,21 @@ Notes: troubleshooting, no matter whether it came from the MCP server or from the user. ### Running Tests -Currently, no automated test suite exists in the repository. +The .NET tests live in `app/Tests`, a single NUnit project that holds the tests of every area; each +area gets its own folder and namespace below it rather than a project of its own. Agents run them +through the IDE for the same reason they build there: + +``` +mcp__rider__execute_terminal_command command: "cd app/Tests && dotnet test" +``` + +An assembly-wide `[SetUpFixture]` in `app/Tests/TestHost.cs` fills the static application state that +the app itself only fills while starting up, `Program.LOGGER_FACTORY` above all. Types that +initialize a static logger from it — `Settings.Provider` among them — otherwise die in their type +initializer before the first assertion. Prefer writing new code so that it does not reach for such +statics at all. + +The Rust tests run with `cargo test` in `runtime/`, through the `rustrover` MCP server. ## Architecture Details diff --git a/app/MindWork AI Studio.sln b/app/MindWork AI Studio.sln index ab62feb1..3666525c 100644 --- a/app/MindWork AI Studio.sln +++ b/app/MindWork AI Studio.sln @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedTools", "SharedTools\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGeneratedMappings", "SourceGeneratedMappings\SourceGeneratedMappings.csproj", "{4D7141D5-9C22-4D85-B748-290D15FF484C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{CD46329B-D135-4594-9A70-55D3480F8FEE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,6 +38,10 @@ Global {4D7141D5-9C22-4D85-B748-290D15FF484C}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D7141D5-9C22-4D85-B748-290D15FF484C}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D7141D5-9C22-4D85-B748-290D15FF484C}.Release|Any CPU.Build.0 = Release|Any CPU + {CD46329B-D135-4594-9A70-55D3480F8FEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD46329B-D135-4594-9A70-55D3480F8FEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD46329B-D135-4594-9A70-55D3480F8FEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD46329B-D135-4594-9A70-55D3480F8FEE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution EndGlobalSection diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index 7f339cd4..14d32189 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -82,6 +82,15 @@ + + + + + diff --git a/app/Tests/Models/TestHarnessTests.cs b/app/Tests/Models/TestHarnessTests.cs new file mode 100644 index 00000000..0ed75de7 --- /dev/null +++ b/app/Tests/Models/TestHarnessTests.cs @@ -0,0 +1,39 @@ +using AIStudio.Provider; +using AIStudio.Settings; + +namespace AIStudio.Tests.Models; + +/// +/// Checks the test harness itself, before any test states something about the app. +/// +/// +/// Three things have to hold before a capability test can mean anything: the app assembly is +/// referenced, this project counts as a friend assembly, and the assembly-wide setup has run. When +/// one of them is missing, the failure looks like a broken rule rather than a broken harness, which +/// is an expensive detour. These two tests make the difference visible right away. +/// +/// Note the fully written type name below. AIStudio.Provider is a namespace and AIStudio.Settings +/// .Provider is a type; inside a namespace under AIStudio, the namespace wins the lookup. That is a +/// property of the app's own naming, not of the tests. +/// +[TestFixture] +public sealed class TestHarnessTests +{ + [Test] + public void TheStaticApplicationStateIsAvailable() + { + // + // Settings.Provider initializes a static logger from Program.LOGGER_FACTORY. Touching it + // without the assembly-wide setup throws a TypeInitializationException. + // + Assert.That(AIStudio.Settings.Provider.NONE.UsedLLMProvider, Is.EqualTo(LLMProviders.NONE)); + } + + [Test] + public void TheCapabilityApiOfTheAppIsReachable() + { + var capabilities = LLMProviders.OPEN_AI.GetModelCapabilities(new Model("gpt-5.1", null)); + + Assert.That(capabilities, Does.Contain(Capability.FUNCTION_CALLING)); + } +} \ No newline at end of file diff --git a/app/Tests/TestHost.cs b/app/Tests/TestHost.cs new file mode 100644 index 00000000..553ce548 --- /dev/null +++ b/app/Tests/TestHost.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Logging.Abstractions; + +// +// Deliberately without a namespace: NUnit then applies this fixture to the whole assembly, so every +// test -- the ones written today and the ones written later in some other folder -- starts with the +// static state below already in place. A second setup fixture would only ever be needed for state +// that must not leak between areas. +// +namespace AIStudio.Tests; + +[SetUpFixture] +public sealed class TestHost +{ + [OneTimeSetUp] + public void PrepareStaticApplicationState() + { + // + // A number of types in the app hold a static logger field that is initialized from + // Program.LOGGER_FACTORY, among them Settings.Provider. The app assigns that factory while + // Kestrel comes up; in a test process nobody does, so it stays null and the first touch of + // such a type dies inside its type initializer -- before a single assertion runs. A factory + // that writes nowhere is all it takes to get past that. + // + Program.LOGGER_FACTORY = NullLoggerFactory.Instance; + } +} \ No newline at end of file diff --git a/app/Tests/Tests.csproj b/app/Tests/Tests.csproj new file mode 100644 index 00000000..6e5fc3ff --- /dev/null +++ b/app/Tests/Tests.csproj @@ -0,0 +1,40 @@ + + + + net9.0 + latest + enable + enable + AIStudio.Tests + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + +