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
+
+
+
+
+
+
+
+
+
+
+
+
+