diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aee30719..bf8eb267 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,11 +170,11 @@ jobs: '| --- | ---: | ---: | ---: | ---: | ---: | --- |' ) | Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY -Encoding utf8 $modules = @( + @{ Name = 'Agentstration.Api.Tests'; Tests = 191; Warning = 1700; Failure = 2048 }, @{ Name = 'Agentstration.Management.Api.Tests'; Tests = 43; Warning = 900; Failure = 1024 }, @{ Name = 'Agentstration.Management.Bootstrap.Tests'; Tests = 23; Warning = 500; Failure = 700 }, @{ Name = 'Agentstration.Management.Security.Tests'; Tests = 38; Warning = 800; Failure = 1024 }, - @{ Name = 'Agentstration.Management.Aep.Tests'; Tests = 11; Warning = 450; Failure = 700 }, - @{ Name = 'Agentstration.Management.Sources.Tests'; Tests = 93; Warning = 550; Failure = 900 } + @{ Name = 'Agentstration.Management.Aep.Tests'; Tests = 11; Warning = 450; Failure = 700 } ) foreach ($module in $modules) { ./scripts/ci/run-test-module-with-diagnostics.ps1 ` diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c683dec..c7b8f77c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,9 +55,9 @@ jobs: - name: Build run: dotnet build Agentstration.slnx --configuration Release --no-restore - name: Test fast lane - run: dotnet test --solution Agentstration.Tests.Fast.slnx --configuration Release --no-build --minimum-expected-tests 139 --max-parallel-test-modules 4 + run: dotnet test --solution Agentstration.Tests.Fast.slnx --configuration Release --no-build --minimum-expected-tests 336 --max-parallel-test-modules 4 - name: Test integration lane - run: dotnet test --solution Agentstration.Tests.Integration.slnx --configuration Release --no-build --minimum-expected-tests 692 --max-parallel-test-modules 2 + run: dotnet test --solution Agentstration.Tests.Integration.slnx --configuration Release --no-build --minimum-expected-tests 515 --max-parallel-test-modules 2 - name: Restore autonomous AEP SDK run: dotnet restore aep/Aep.slnx -p:NuGetAudit=true -p:NuGetAuditMode=all - name: Build autonomous AEP SDK diff --git a/AGENTS.md b/AGENTS.md index 116ce84d..d64038c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -187,8 +187,8 @@ Run from the repository root: ```powershell dotnet restore Agentstration.slnx dotnet build Agentstration.slnx --configuration Release --no-restore -dotnet test --solution Agentstration.Tests.Fast.slnx --configuration Release --no-build --minimum-expected-tests 139 --max-parallel-test-modules 4 -dotnet test --solution Agentstration.Tests.Integration.slnx --configuration Release --no-build --minimum-expected-tests 692 --max-parallel-test-modules 2 +dotnet test --solution Agentstration.Tests.Fast.slnx --configuration Release --no-build --minimum-expected-tests 336 --max-parallel-test-modules 4 +dotnet test --solution Agentstration.Tests.Integration.slnx --configuration Release --no-build --minimum-expected-tests 515 --max-parallel-test-modules 2 ``` The two test solutions together form the required deterministic, offline functional suite. Performance and live-provider workloads are explicit opt-ins documented in `docs/contributing/testing.md`. For a focused iteration, run the affected test project first, then run both functional lanes before handoff. Do not suppress warnings or disable analyzers to make a change pass. diff --git a/Agentstration.Tests.Fast.slnx b/Agentstration.Tests.Fast.slnx index cab470cd..265ece18 100644 --- a/Agentstration.Tests.Fast.slnx +++ b/Agentstration.Tests.Fast.slnx @@ -1,6 +1,8 @@ + + diff --git a/Agentstration.Tests.Integration.slnx b/Agentstration.Tests.Integration.slnx index c502a14a..c5fcfff3 100644 --- a/Agentstration.Tests.Integration.slnx +++ b/Agentstration.Tests.Integration.slnx @@ -1,5 +1,6 @@ + @@ -11,7 +12,6 @@ - diff --git a/Agentstration.slnx b/Agentstration.slnx index 2115a766..0af215a1 100644 --- a/Agentstration.slnx +++ b/Agentstration.slnx @@ -56,8 +56,11 @@ + + + @@ -73,7 +76,6 @@ - diff --git a/docs/architecture.md b/docs/architecture.md index 8e4217f2..e6f95c72 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -47,8 +47,11 @@ src/ Agentstration.Work.Storage.Abstractions/ Agentstration.Work.Storage.Sqlite/ tests/ + Agentstration.Api.Tests/ Agentstration.Application.Tests/ Agentstration.ArchitectureTests/ + Agentstration.Console.Client.Tests/ + Agentstration.Console.Components.Tests/ Agentstration.Management.Core.Tests/ Agentstration.Management.Storage.Tests/ Agentstration.Management.Sources.Tests/ diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md index afe2b174..69e156b7 100644 --- a/docs/contributing/testing.md +++ b/docs/contributing/testing.md @@ -7,7 +7,7 @@ Agentstration separates test execution by runtime cost and dependency type. The Fast tests do not start an ASP.NET Core host, open a relational database, spawn a child process, or contact a remote provider. They cover architecture rules, component behavior, mapping, validation, and in-process command behavior. ```powershell -dotnet test --solution Agentstration.Tests.Fast.slnx --configuration Release --no-build --minimum-expected-tests 139 --max-parallel-test-modules 4 +dotnet test --solution Agentstration.Tests.Fast.slnx --configuration Release --no-build --minimum-expected-tests 336 --max-parallel-test-modules 4 ``` ## Integration lane @@ -15,7 +15,7 @@ dotnet test --solution Agentstration.Tests.Fast.slnx --configuration Release --n Integration tests exercise a real boundary such as `WebApplicationFactory`, SQLite, Git, persistent Identity, or runtime reconstruction. This lane remains deterministic and offline by default. Tests marked `Integration` for a live model provider are opt-in and report inconclusive unless their documented environment variables are supplied. ```powershell -dotnet test --solution Agentstration.Tests.Integration.slnx --configuration Release --no-build --minimum-expected-tests 692 --max-parallel-test-modules 2 +dotnet test --solution Agentstration.Tests.Integration.slnx --configuration Release --no-build --minimum-expected-tests 515 --max-parallel-test-modules 2 ``` Run both fast and integration solutions for complete required functional validation. Their union is the functional test inventory represented by the root solution. @@ -47,41 +47,43 @@ The report records workload parameters, provider, elapsed time, runtime and OS m ## CI concurrency and memory diagnostics -The fast, integration, and performance lanes cap concurrent test modules at 4, 2, and 1 respectively. Host-heavy Management modules also use one class worker per assembly. CI reruns the designated hosted modules sequentially through `scripts/ci/run-test-module-with-diagnostics.ps1`; each JSON artifact contains the discovered count, duration, process peak working set, process peak private memory, aggregate peak working set for active `dotnet` processes, runtime, and OS. The diagnostic artifact deliberately excludes test output and payloads. +The fast, integration, and performance lanes cap concurrent test modules at 4, 2, and 1 respectively. Host-heavy API and Management modules also use one class worker per assembly. CI reruns the designated hosted modules sequentially through `scripts/ci/run-test-module-with-diagnostics.ps1`; each JSON artifact contains the discovered count, duration, process peak working set, process peak private memory, aggregate peak working set for active `dotnet` processes, runtime, and OS. The diagnostic artifact deliberately excludes test output and payloads. -The initial budgets below use Release runs on Windows 11 10.0.26200 with .NET 10.0.10/10.0.11, collected during #298. A warning is evidence to review the Linux and Windows trend; a failure protects constrained runners from returning to the original greater-than-1-GiB process. Adjust these values only after retaining representative artifacts from both runner families. +The Management budgets below use Release runs on Windows 11 10.0.26200 with .NET 10.0.10/10.0.11, collected during #298. The consolidated `Agentstration.Api.Tests` baseline was collected on the same Windows build with .NET 10.0.11 during #142: 191 tests in 200 seconds, 1503.7 MiB peak working set, 1176.6 MiB peak private memory, and 1621.2 MiB aggregate `dotnet` working set. A warning is evidence to review the Linux and Windows trend; a failure caps regression relative to the retained baseline. Adjust these values only after retaining representative artifacts from both runner families. | Module | Baseline peak (MiB) | Warning (MiB) | Failure (MiB) | Minimum tests | | --- | ---: | ---: | ---: | ---: | +| `Agentstration.Api.Tests` | 1503.7 | 1700 | 2048 | 191 | | `Agentstration.Management.Api.Tests` | 749.3 | 900 | 1024 | 43 | | `Agentstration.Management.Bootstrap.Tests` | 355.3 | 500 | 700 | 23 | | `Agentstration.Management.Security.Tests` | 730.1 | 800 | 1024 | 38 | | `Agentstration.Management.Aep.Tests` | 322.1 | 450 | 700 | 11 | -| `Agentstration.Management.Sources.Tests` | 431.5 | 550 | 900 | 93 | ## Project classification | Test project | Lane | Boundary or rationale | | --- | --- | --- | | `Agentstration.ArchitectureTests` | Fast | Assembly dependency rules | +| `Agentstration.Console.Client.Tests` | Fast | HTTP and SignalR mappings, pagination, errors, retries, and credential forwarding without an authoritative server | +| `Agentstration.Console.Components.Tests` | Fast | Console Razor behavior, presentation state, permissions, localization, and static assets with mocked clients | | `Agentstration.Management.Core.Tests` | Fast | Pure Management validation and in-memory use cases | | `Agentstration.Tools.SourceRegistry.Tests` | Fast | In-process CLI and manifest validation | | `Agentstration.Web.Components.Tests` | Fast | bUnit component behavior | | `Agentstration.Web.FlowDesigner.Tests` | Fast | bUnit and graph projection behavior | | `Agentstration.Workplace.Components.Tests` | Fast | bUnit component behavior | -| `Agentstration.Application.Tests` | Integration | Includes SQLite Flow and Work storage contracts | +| `Agentstration.Application.Tests` | Integration | Application use cases and their SQLite-backed contracts without the executable host | | `Agentstration.Management.Storage.Tests` | Integration | SQLite control-plane, Identity persistence, secrets, audit, and trigger storage | -| `Agentstration.Management.Sources.Tests` | Integration | Source, registry, provider, and Pack distribution boundaries | +| `Agentstration.Management.Sources.Tests` | Integration | Source provider, registry transport, and Pack composition behavior without the executable host | +| `Agentstration.Api.Tests` | Integration | Hosted HTTP, authentication, OpenAPI, MCP, SignalR, Flow, Runtime, Work, and cross-cutting transport behavior | | `Agentstration.Management.Api.Tests` | Integration | Hosted Management API tests using the API-only test profile | | `Agentstration.Management.Bootstrap.Tests` | Integration | Declarative bootstrap catalog, application, and hosted startup scenarios using the API-only test profile | | `Agentstration.Management.Security.Tests` | Integration | Identity, authorization, local-account, and interactive Security boundaries | | `Agentstration.Management.Aep.Tests` | Integration | AEP enrollment lifecycle and extension inventory boundaries using the API-only test profile | | `Agentstration.ModelProviders.Tests` | Integration, provider-optional | AEP test hosts plus opt-in live-provider checks | -| `Agentstration.Runtime.Tests` | Integration | SQLite reconstruction and hosted runtime endpoints | +| `Agentstration.Runtime.Tests` | Integration | Runtime business behavior and SQLite reconstruction without the executable host | | `Agentstration.SourceProviders.Git.Tests` | Integration | Real Git processes and file-system repositories | -| `Agentstration.Web.Tests` | Integration | Full Web host plus a bounded deterministic SQLite contention correctness smoke | +| `Agentstration.Web.Tests` | Integration | Combined standalone composition, startup, storage profiles, workers, and lifecycle | | `Agentstration.Performance.Tests` | Performance, opt-in | SQLite/PostgreSQL relational storage concurrency workloads and machine-readable reports | -| `Agentstration.Work.Api.Tests` | Integration | Full Work API host | | `Agentstration.Workplace.Web.Tests` | Integration | Workplace HTTP host | The autonomous AEP SDK keeps its own `aep/Aep.slnx` validation because it can be built and released independently from the product solution. diff --git a/scripts/ci/run-functional-coverage.ps1 b/scripts/ci/run-functional-coverage.ps1 index 2a95037a..f7e8fbc1 100644 --- a/scripts/ci/run-functional-coverage.ps1 +++ b/scripts/ci/run-functional-coverage.ps1 @@ -24,14 +24,14 @@ $lanes = @( @{ Name = 'Fast' Solution = 'Agentstration.Tests.Fast.slnx' - MinimumTests = 139 + MinimumTests = 336 ParallelModules = 4 ResultsDirectory = Join-Path $resolvedCoverageRoot 'raw/fast' }, @{ Name = 'Integration' Solution = 'Agentstration.Tests.Integration.slnx' - MinimumTests = 692 + MinimumTests = 515 ParallelModules = 2 ResultsDirectory = Join-Path $resolvedCoverageRoot 'raw/integration' } diff --git a/src/Agentstration.Api/Agentstration.Api.csproj b/src/Agentstration.Api/Agentstration.Api.csproj index 68277549..ab547451 100644 --- a/src/Agentstration.Api/Agentstration.Api.csproj +++ b/src/Agentstration.Api/Agentstration.Api.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Agentstration.Console.Components/Agentstration.Console.Components.csproj b/src/Agentstration.Console.Components/Agentstration.Console.Components.csproj index d64aee1c..3fa9553a 100644 --- a/src/Agentstration.Console.Components/Agentstration.Console.Components.csproj +++ b/src/Agentstration.Console.Components/Agentstration.Console.Components.csproj @@ -4,7 +4,7 @@ Agentstration.Web - + diff --git a/src/Agentstration.Web/README.md b/src/Agentstration.Web/README.md index 7eaa332e..6813083e 100644 --- a/src/Agentstration.Web/README.md +++ b/src/Agentstration.Web/README.md @@ -108,4 +108,4 @@ Interactive Server components invoke canonical APIs through server-side typed cl ## Tests -`Agentstration.Web.Tests` covers API client mapping, model discovery, profile filtering and selection rules, conditional request headers, Problem Details, editor and runner payload mapping, simulated CRUD, SSE processing, retry, and dashboard aggregation. `Agentstration.Web.Components.Tests` covers focused UI state services. Both use MSTest and remain offline. +`Agentstration.Api.Tests` owns hosted transport behavior. `Agentstration.Console.Client.Tests` covers client mappings, conditional requests, Problem Details, streaming, and retries without starting the authoritative server. `Agentstration.Console.Components.Tests` covers Console presentation behavior with mocked clients. `Agentstration.Web.Tests` is reserved for combined standalone composition, startup, storage, workers, and lifecycle. All use MSTest and remain offline by default. diff --git a/tests/Agentstration.Api.Tests/Agentstration.Api.Tests.csproj b/tests/Agentstration.Api.Tests/Agentstration.Api.Tests.csproj new file mode 100644 index 00000000..a527c8f4 --- /dev/null +++ b/tests/Agentstration.Api.Tests/Agentstration.Api.Tests.csproj @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Agentstration.Api.Tests/ApiAuthenticationRegistrationTests.cs b/tests/Agentstration.Api.Tests/ApiAuthenticationRegistrationTests.cs new file mode 100644 index 00000000..d26f0cbf --- /dev/null +++ b/tests/Agentstration.Api.Tests/ApiAuthenticationRegistrationTests.cs @@ -0,0 +1,84 @@ +using Agentstration.Web; +using Agentstration.Web.Configuration; +using Agentstration.Web.Security; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace Agentstration.Api.Tests; + +[TestClass] +public sealed class ApiAuthenticationRegistrationTests +{ + [TestMethod] + public void OidcApisPreferBearerButAcceptTheTrustedConsoleSession() + { + var services = new ServiceCollection(); + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["Agentstration:Authentication:Mode"] = ApiAuthenticationOptions.Oidc, + ["Agentstration:Authentication:Authority"] = "https://identity.example/", + ["Agentstration:Authentication:Audience"] = "agentstration-api", + ["Agentstration:Authentication:ClientId"] = "agentstration-console" + }).Build(); + services.AddLogging(); + services.AddAgentstrationApi(configuration, new TestHostEnvironment()); + using var provider = services.BuildServiceProvider(); + var selector = provider.GetRequiredService>() + .Get(AgentstrationAuthenticationDefaults.PolicyScheme).ForwardDefaultSelector; + Assert.IsNotNull(selector); + + var api = new DefaultHttpContext(); + api.Request.Path = "/api/agents"; + Assert.AreEqual(JwtBearerDefaults.AuthenticationScheme, selector(api)); + + api.Request.Headers.Cookie = $"{AgentstrationAuthenticationDefaults.ApplicationCookie}=session"; + Assert.AreEqual(IdentityConstants.ApplicationScheme, selector(api)); + + api.Request.Headers.Authorization = "Bearer access-token"; + Assert.AreEqual(JwtBearerDefaults.AuthenticationScheme, selector(api)); + } + + [TestMethod] + public async Task CookieAuthenticationReturnsStatusCodeInsteadOfHtmlRedirectForHubs() + { + var services = new ServiceCollection(); + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["Agentstration:Authentication:Mode"] = ApiAuthenticationOptions.Local + }).Build(); + services.AddLogging(); + services.AddAgentstrationApi(configuration, new TestHostEnvironment()); + using var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>() + .Get(IdentityConstants.ApplicationScheme); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Path = "/hubs/flow-runs/negotiate"; + var redirect = new RedirectContext( + httpContext, + new AuthenticationScheme(IdentityConstants.ApplicationScheme, null, typeof(CookieAuthenticationHandler)), + options, + new AuthenticationProperties(), + "/login"); + + await options.Events.OnRedirectToLogin(redirect); + + Assert.AreEqual(StatusCodes.Status401Unauthorized, httpContext.Response.StatusCode); + Assert.IsFalse(httpContext.Response.Headers.ContainsKey("Location")); + } + + private sealed class TestHostEnvironment : IHostEnvironment + { + public string EnvironmentName { get; set; } = "Testing"; + public string ApplicationName { get; set; } = nameof(ApiAuthenticationRegistrationTests); + public string ContentRootPath { get; set; } = AppContext.BaseDirectory; + public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); + } +} diff --git a/tests/Agentstration.Api.Tests/AssemblyInfo.cs b/tests/Agentstration.Api.Tests/AssemblyInfo.cs new file mode 100644 index 00000000..3119b2d6 --- /dev/null +++ b/tests/Agentstration.Api.Tests/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[assembly: DoNotParallelize] diff --git a/tests/Agentstration.Application.Tests/FlowCallAuthoringTests.cs b/tests/Agentstration.Api.Tests/FlowCallAuthoringTests.cs similarity index 100% rename from tests/Agentstration.Application.Tests/FlowCallAuthoringTests.cs rename to tests/Agentstration.Api.Tests/FlowCallAuthoringTests.cs diff --git a/tests/Agentstration.Application.Tests/FlowNestedRunTests.cs b/tests/Agentstration.Api.Tests/FlowNestedRunTests.cs similarity index 100% rename from tests/Agentstration.Application.Tests/FlowNestedRunTests.cs rename to tests/Agentstration.Api.Tests/FlowNestedRunTests.cs diff --git a/tests/Agentstration.Application.Tests/FlowRunCausalityTests.cs b/tests/Agentstration.Api.Tests/FlowRunCausalityTests.cs similarity index 100% rename from tests/Agentstration.Application.Tests/FlowRunCausalityTests.cs rename to tests/Agentstration.Api.Tests/FlowRunCausalityTests.cs diff --git a/tests/Agentstration.Application.Tests/FlowStorageQueryContractTests.cs b/tests/Agentstration.Api.Tests/FlowStorageQueryContractTests.cs similarity index 100% rename from tests/Agentstration.Application.Tests/FlowStorageQueryContractTests.cs rename to tests/Agentstration.Api.Tests/FlowStorageQueryContractTests.cs diff --git a/tests/Agentstration.Application.Tests/FlowTests.Authoring.cs b/tests/Agentstration.Api.Tests/FlowTests.Authoring.cs similarity index 100% rename from tests/Agentstration.Application.Tests/FlowTests.Authoring.cs rename to tests/Agentstration.Api.Tests/FlowTests.Authoring.cs diff --git a/tests/Agentstration.Application.Tests/FlowTests.Execution.cs b/tests/Agentstration.Api.Tests/FlowTests.Execution.cs similarity index 100% rename from tests/Agentstration.Application.Tests/FlowTests.Execution.cs rename to tests/Agentstration.Api.Tests/FlowTests.Execution.cs diff --git a/tests/Agentstration.Application.Tests/FlowTests.InteractionAndRecovery.cs b/tests/Agentstration.Api.Tests/FlowTests.InteractionAndRecovery.cs similarity index 100% rename from tests/Agentstration.Application.Tests/FlowTests.InteractionAndRecovery.cs rename to tests/Agentstration.Api.Tests/FlowTests.InteractionAndRecovery.cs diff --git a/tests/Agentstration.Application.Tests/FlowTests.SecurityAndApi.cs b/tests/Agentstration.Api.Tests/FlowTests.SecurityAndApi.cs similarity index 100% rename from tests/Agentstration.Application.Tests/FlowTests.SecurityAndApi.cs rename to tests/Agentstration.Api.Tests/FlowTests.SecurityAndApi.cs diff --git a/tests/Agentstration.Application.Tests/FlowTests.cs b/tests/Agentstration.Api.Tests/FlowTests.cs similarity index 100% rename from tests/Agentstration.Application.Tests/FlowTests.cs rename to tests/Agentstration.Api.Tests/FlowTests.cs diff --git a/tests/Agentstration.Web.Tests/McpToolCatalogTests.cs b/tests/Agentstration.Api.Tests/McpToolCatalogTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/McpToolCatalogTests.cs rename to tests/Agentstration.Api.Tests/McpToolCatalogTests.cs diff --git a/tests/Agentstration.Web.Tests/OpenApiTests.cs b/tests/Agentstration.Api.Tests/OpenApiTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/OpenApiTests.cs rename to tests/Agentstration.Api.Tests/OpenApiTests.cs diff --git a/tests/Agentstration.Management.Sources.Tests/PackTests.cs b/tests/Agentstration.Api.Tests/PackTests.cs similarity index 100% rename from tests/Agentstration.Management.Sources.Tests/PackTests.cs rename to tests/Agentstration.Api.Tests/PackTests.cs diff --git a/tests/Agentstration.Runtime.Tests/RuntimeRunTests.cs b/tests/Agentstration.Api.Tests/RuntimeRunTests.cs similarity index 100% rename from tests/Agentstration.Runtime.Tests/RuntimeRunTests.cs rename to tests/Agentstration.Api.Tests/RuntimeRunTests.cs diff --git a/tests/Agentstration.Management.Sources.Tests/SourceRegistryManagementTests.cs b/tests/Agentstration.Api.Tests/SourceRegistryManagementTests.cs similarity index 100% rename from tests/Agentstration.Management.Sources.Tests/SourceRegistryManagementTests.cs rename to tests/Agentstration.Api.Tests/SourceRegistryManagementTests.cs diff --git a/tests/Agentstration.Management.Sources.Tests/SourceTests.cs b/tests/Agentstration.Api.Tests/SourceTests.cs similarity index 100% rename from tests/Agentstration.Management.Sources.Tests/SourceTests.cs rename to tests/Agentstration.Api.Tests/SourceTests.cs diff --git a/tests/Agentstration.Web.Tests/ToolGovernanceAuditEndpointTests.cs b/tests/Agentstration.Api.Tests/ToolGovernanceAuditEndpointTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ToolGovernanceAuditEndpointTests.cs rename to tests/Agentstration.Api.Tests/ToolGovernanceAuditEndpointTests.cs diff --git a/tests/Agentstration.Application.Tests/WorkPlaneTests.cs b/tests/Agentstration.Api.Tests/WorkPlaneTests.cs similarity index 100% rename from tests/Agentstration.Application.Tests/WorkPlaneTests.cs rename to tests/Agentstration.Api.Tests/WorkPlaneTests.cs diff --git a/tests/Agentstration.Work.Api.Tests/WorkplaceApiTests.cs b/tests/Agentstration.Api.Tests/WorkplaceApiTests.cs similarity index 99% rename from tests/Agentstration.Work.Api.Tests/WorkplaceApiTests.cs rename to tests/Agentstration.Api.Tests/WorkplaceApiTests.cs index 07ffc7c3..99142291 100644 --- a/tests/Agentstration.Work.Api.Tests/WorkplaceApiTests.cs +++ b/tests/Agentstration.Api.Tests/WorkplaceApiTests.cs @@ -1,5 +1,3 @@ -extern alias workapi; - using System.Net; using System.Net.Http.Json; using System.Text.Json; @@ -19,9 +17,9 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; -using WorkApiProgram = workapi::Program; +using WorkApiProgram = global::Program; -namespace Agentstration.Work.Api.Tests; +namespace Agentstration.Api.Tests; [TestClass] public sealed class WorkplaceApiTests diff --git a/tests/Agentstration.Application.Tests/Agentstration.Application.Tests.csproj b/tests/Agentstration.Application.Tests/Agentstration.Application.Tests.csproj index cbe0dbeb..c8908462 100644 --- a/tests/Agentstration.Application.Tests/Agentstration.Application.Tests.csproj +++ b/tests/Agentstration.Application.Tests/Agentstration.Application.Tests.csproj @@ -1,6 +1,5 @@ - @@ -8,13 +7,9 @@ - - - - diff --git a/tests/Agentstration.ArchitectureTests/DependencyTests.cs b/tests/Agentstration.ArchitectureTests/DependencyTests.cs index 5d6a4506..fe9dcca1 100644 --- a/tests/Agentstration.ArchitectureTests/DependencyTests.cs +++ b/tests/Agentstration.ArchitectureTests/DependencyTests.cs @@ -594,6 +594,77 @@ public void ProjectsDoNotCompileSourcesOwnedByOtherProjects() Assert.IsEmpty(violations, $"Projects must not compile source files owned by another project:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}"); } + [TestMethod] + public void TestProjectsReflectApiConsoleAndStandaloneHostOwnership() + { + var testsRoot = Path.Combine(FindRepositoryRoot(), "tests"); + foreach (var project in new[] + { + "Agentstration.Api.Tests", + "Agentstration.Console.Client.Tests", + "Agentstration.Console.Components.Tests", + "Agentstration.Web.Tests" + }) + { + Assert.IsTrue(File.Exists(Path.Combine(testsRoot, project, $"{project}.csproj")), $"Missing {project}."); + } + + Assert.IsFalse(File.Exists(Path.Combine(testsRoot, "Agentstration.Work.Api.Tests", "Agentstration.Work.Api.Tests.csproj"))); + } + + [TestMethod] + public void BusinessTestProjectsDoNotReferenceTheApiOrExecutableHost() + { + var testsRoot = Path.Combine(FindRepositoryRoot(), "tests"); + foreach (var project in new[] + { + "Agentstration.Application.Tests", + "Agentstration.Runtime.Tests", + "Agentstration.Management.Core.Tests", + "Agentstration.Management.Sources.Tests", + "Agentstration.Management.Storage.Tests" + }) + { + var contents = File.ReadAllText(Path.Combine(testsRoot, project, $"{project}.csproj")); + Assert.DoesNotContain("src/Agentstration.Api/", contents, StringComparison.Ordinal, project); + Assert.DoesNotContain("src/Agentstration.Web/", contents, StringComparison.Ordinal, project); + Assert.DoesNotContain("Microsoft.AspNetCore.Mvc.Testing", contents, StringComparison.Ordinal, project); + } + } + + [TestMethod] + public void ConsoleTestProjectsDoNotDependOnTheAuthoritativeServer() + { + var testsRoot = Path.Combine(FindRepositoryRoot(), "tests"); + var clientRoot = Path.Combine(testsRoot, "Agentstration.Console.Client.Tests"); + var componentRoot = Path.Combine(testsRoot, "Agentstration.Console.Components.Tests"); + + foreach (var projectRoot in new[] { clientRoot, componentRoot }) + { + var project = File.ReadAllText(Directory.EnumerateFiles(projectRoot, "*.csproj").Single()); + var sources = string.Join(Environment.NewLine, Directory.EnumerateFiles(projectRoot, "*.cs").Select(File.ReadAllText)); + Assert.DoesNotContain("src/Agentstration.Api/", project, StringComparison.Ordinal); + Assert.DoesNotContain("src/Agentstration.Web/", project, StringComparison.Ordinal); + Assert.DoesNotContain("WebApplicationFactory", sources, StringComparison.Ordinal); + Assert.DoesNotContain("Agentstration.Web.Api", sources, StringComparison.Ordinal); + Assert.DoesNotContain("Agentstration.Web.Hosting", sources, StringComparison.Ordinal); + Assert.DoesNotContain("Agentstration.Web.Security", sources, StringComparison.Ordinal); + } + } + + [TestMethod] + public void StandaloneHostTestsDoNotOwnClientOrComponentTestInfrastructure() + { + var project = File.ReadAllText(Path.Combine( + FindRepositoryRoot(), "tests", "Agentstration.Web.Tests", "Agentstration.Web.Tests.csproj")); + + Assert.DoesNotContain( + " + + + + + + + + + + + + + + + diff --git a/tests/Agentstration.Web.Tests/ApiClientTests.Bootstrap.cs b/tests/Agentstration.Console.Client.Tests/ApiClientTests.Bootstrap.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ApiClientTests.Bootstrap.cs rename to tests/Agentstration.Console.Client.Tests/ApiClientTests.Bootstrap.cs diff --git a/tests/Agentstration.Web.Tests/ApiClientTests.Flow.cs b/tests/Agentstration.Console.Client.Tests/ApiClientTests.Flow.cs similarity index 67% rename from tests/Agentstration.Web.Tests/ApiClientTests.Flow.cs rename to tests/Agentstration.Console.Client.Tests/ApiClientTests.Flow.cs index 361cc2f7..f87b8bfd 100644 --- a/tests/Agentstration.Web.Tests/ApiClientTests.Flow.cs +++ b/tests/Agentstration.Console.Client.Tests/ApiClientTests.Flow.cs @@ -10,23 +10,9 @@ using Agentstration.Runtime.Abstractions; using Agentstration.Runtime.Contracts; using Agentstration.Web.Components; -using Agentstration.Web.Configuration; using Agentstration.Web.Console; -using Agentstration.Web.Features.Flows.Designer; -using Agentstration.Web.FlowDesigner.Backend; -using Agentstration.Web.Security; using Agentstration.Work; using Agentstration.Work.Contracts; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; namespace Agentstration.Web.Tests; @@ -117,49 +103,6 @@ public async Task EntryResourcePickerLoadsFlowsFromCanonicalFlowApiInsteadOfWork Assert.IsEmpty(workRequests); } - [TestMethod] - public async Task FlowDesignerMaterializesDraftFromActivePublishedVersion() - { - var now = new DateTimeOffset(2026, 8, 6, 12, 0, 0, TimeSpan.Zero); - var flowId = new FlowId("universal-router"); - var definition = new DirectFlowDefinition(new FlowTargetReference(FlowTargetKind.Agent, "agent-id")); - var flow = new FlowResponse(flowId.Value, flowId.Value, null, "1.0.0", true, "1.0.0", definition, new Dictionary(), now, now); - var draft = new FlowDraftResponse(new FlowDraft - { - WorkspaceId = TestWorkspaceId, - Id = "draft-universal-router", - FlowId = flowId, - DisplayName = "Universal router", - Definition = new FlowGraphDefinition { EntryStep = "input", Steps = [new InputFlowStepDefinition { Name = "input" }], Transitions = [] }, - CreatedAt = now, - UpdatedAt = now - }, "\"draft-etag\""); - var requests = new List(); - using var httpClient = new HttpClient(new StubHandler(request => - { - requests.Add($"{request.Method} {request.RequestUri!.AbsolutePath}"); - if (request.Method == HttpMethod.Get && request.RequestUri.AbsolutePath.EndsWith("/draft", StringComparison.Ordinal)) - return new HttpResponseMessage(HttpStatusCode.NotFound) { Content = JsonContent.Create(new { title = "flow_draft_not_found", status = 404 }) }; - if (request.Method == HttpMethod.Get && request.RequestUri.AbsolutePath.EndsWith("/draft/source", StringComparison.Ordinal)) - return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(new FlowSourceResponse("entryStep: input", "yaml", 1)) }; - if (request.Method == HttpMethod.Get) - return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(flow) }; - return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(draft) }; - })) - { BaseAddress = new Uri("http://localhost/") }; - - var actual = await new FlowDesignerBackend(new FlowApiClient(httpClient)).LoadAsync(new(ResourceNamespace.Default, flowId.Value), default); - - Assert.AreEqual(flowId, actual.Resource.FlowId); - CollectionAssert.AreEqual(new[] - { - "GET /api/flows/universal-router/draft", - "GET /api/flows/universal-router", - "POST /api/flows/universal-router/versions/1.0.0/draft", - "GET /api/flows/universal-router/draft/source" - }, requests); - } - [TestMethod] public async Task FlowAuthoringClientPreservesETagAndPublishesImmutableVersion() { @@ -357,60 +300,4 @@ await Assert.ThrowsExactlyAsync(() => Assert.AreEqual(1, requests); } - [TestMethod] - public async Task FlowDesignerLoadsNamespacedPublishedGraphWithoutDraftCallsAndRejectsMutations() - { - var now = new DateTimeOffset(2026, 8, 15, 12, 0, 0, TimeSpan.Zero); - var @namespace = new ResourceNamespace("pack.sample"); - var graph = new FlowGraphDefinition { EntryStep = "input", Steps = [new InputFlowStepDefinition { Name = "input" }], Transitions = [] }; - var definition = new DirectFlowDefinition(new FlowTargetReference(FlowTargetKind.Agent, "agent-id")); - var flow = new FlowResponse("sample", "Pack sample", null, "1.2.0", true, "1.2.0", definition, new Dictionary(), now, now) { Namespace = @namespace }; - var version = new FlowVersionResponse("sample", "1.2.0", null, definition, new Dictionary(), now, graph) { Namespace = @namespace }; - var requests = new List(); - using var httpClient = new HttpClient(new StubHandler(request => - { - requests.Add($"{request.Method} {request.RequestUri!.AbsolutePath}"); - return request.RequestUri.AbsolutePath.EndsWith("/versions/1.2.0", StringComparison.Ordinal) - ? new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(version) } - : new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(flow) }; - })) - { BaseAddress = new Uri("http://localhost/") }; - var backend = new FlowDesignerBackend(new FlowApiClient(httpClient)); - var target = new FlowDesignerTarget(@namespace, "sample"); - - var loaded = await backend.LoadAsync(target, default); - - Assert.AreEqual("1.2.0", loaded.PublishedVersion); - StringAssert.Contains(loaded.Source, "entryStep: input"); - CollectionAssert.AreEqual(new[] - { - "GET /api/namespaces/pack.sample/flows/sample", - "GET /api/namespaces/pack.sample/flows/sample/versions/1.2.0" - }, requests); - await Assert.ThrowsExactlyAsync(() => backend.SaveDraftAsync(target, new("Sample", null, null, graph), string.Empty, default)); - await Assert.ThrowsExactlyAsync(() => backend.ReplaceSourceAsync(target, new("entryStep: input"), string.Empty, default)); - await Assert.ThrowsExactlyAsync(() => backend.PublishAsync(target, new("1.3.0"), default)); - using var input = JsonDocument.Parse("{}"); - await Assert.ThrowsExactlyAsync(() => backend.RunDraftAsync(target, new(input.RootElement.Clone()), default)); - Assert.HasCount(2, requests); - } - - [TestMethod] - public async Task FlowDesignerReportsLegacyNamespacedVersionWithoutGraph() - { - var now = new DateTimeOffset(2026, 8, 15, 12, 0, 0, TimeSpan.Zero); - var @namespace = new ResourceNamespace("pack.legacy"); - var definition = new DirectFlowDefinition(new FlowTargetReference(FlowTargetKind.Agent, "agent-id")); - var flow = new FlowResponse("legacy", "Legacy", null, "1.0.0", true, "1.0.0", definition, new Dictionary(), now, now) { Namespace = @namespace }; - var version = new FlowVersionResponse("legacy", "1.0.0", null, definition, new Dictionary(), now) { Namespace = @namespace }; - using var httpClient = new HttpClient(new StubHandler(request => request.RequestUri!.AbsolutePath.EndsWith("/versions/1.0.0", StringComparison.Ordinal) - ? new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(version) } - : new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(flow) })) - { BaseAddress = new Uri("http://localhost/") }; - - var exception = await Assert.ThrowsExactlyAsync(() => new FlowDesignerBackend(new FlowApiClient(httpClient)).LoadAsync(new(@namespace, "legacy"), default)); - - StringAssert.Contains(exception.Message, "legacy Flow version without a Graph"); - } - } diff --git a/tests/Agentstration.Web.Tests/ApiClientTests.ManagementAndModels.cs b/tests/Agentstration.Console.Client.Tests/ApiClientTests.ManagementAndModels.cs similarity index 97% rename from tests/Agentstration.Web.Tests/ApiClientTests.ManagementAndModels.cs rename to tests/Agentstration.Console.Client.Tests/ApiClientTests.ManagementAndModels.cs index dea61231..ddeb3810 100644 --- a/tests/Agentstration.Web.Tests/ApiClientTests.ManagementAndModels.cs +++ b/tests/Agentstration.Console.Client.Tests/ApiClientTests.ManagementAndModels.cs @@ -10,23 +10,9 @@ using Agentstration.Runtime.Abstractions; using Agentstration.Runtime.Contracts; using Agentstration.Web.Components; -using Agentstration.Web.Configuration; using Agentstration.Web.Console; -using Agentstration.Web.Features.Flows.Designer; -using Agentstration.Web.FlowDesigner.Backend; -using Agentstration.Web.Security; using Agentstration.Work; using Agentstration.Work.Contracts; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; namespace Agentstration.Web.Tests; diff --git a/tests/Agentstration.Web.Tests/ApiClientTests.RuntimeAndEditors.cs b/tests/Agentstration.Console.Client.Tests/ApiClientTests.RuntimeAndEditors.cs similarity index 95% rename from tests/Agentstration.Web.Tests/ApiClientTests.RuntimeAndEditors.cs rename to tests/Agentstration.Console.Client.Tests/ApiClientTests.RuntimeAndEditors.cs index da31ab77..f7a0bf09 100644 --- a/tests/Agentstration.Web.Tests/ApiClientTests.RuntimeAndEditors.cs +++ b/tests/Agentstration.Console.Client.Tests/ApiClientTests.RuntimeAndEditors.cs @@ -10,23 +10,9 @@ using Agentstration.Runtime.Abstractions; using Agentstration.Runtime.Contracts; using Agentstration.Web.Components; -using Agentstration.Web.Configuration; using Agentstration.Web.Console; -using Agentstration.Web.Features.Flows.Designer; -using Agentstration.Web.FlowDesigner.Backend; -using Agentstration.Web.Security; using Agentstration.Work; using Agentstration.Work.Contracts; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; namespace Agentstration.Web.Tests; diff --git a/tests/Agentstration.Web.Tests/ApiClientTests.ToolDefinitions.cs b/tests/Agentstration.Console.Client.Tests/ApiClientTests.ToolDefinitions.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ApiClientTests.ToolDefinitions.cs rename to tests/Agentstration.Console.Client.Tests/ApiClientTests.ToolDefinitions.cs diff --git a/tests/Agentstration.Web.Tests/ApiClientTests.Triggers.cs b/tests/Agentstration.Console.Client.Tests/ApiClientTests.Triggers.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ApiClientTests.Triggers.cs rename to tests/Agentstration.Console.Client.Tests/ApiClientTests.Triggers.cs diff --git a/tests/Agentstration.Console.Client.Tests/ApiClientTests.WorkplaceAndAuth.cs b/tests/Agentstration.Console.Client.Tests/ApiClientTests.WorkplaceAndAuth.cs new file mode 100644 index 00000000..b49424f9 --- /dev/null +++ b/tests/Agentstration.Console.Client.Tests/ApiClientTests.WorkplaceAndAuth.cs @@ -0,0 +1,100 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using Agentstration.Flow; +using Agentstration.Flow.Contracts; +using Agentstration.Management.Abstractions; +using Agentstration.Management.Contracts; +using Agentstration.Resources; +using Agentstration.Runtime.Abstractions; +using Agentstration.Runtime.Contracts; +using Agentstration.Web.Components; +using Agentstration.Web.Console; +using Agentstration.Work; +using Agentstration.Work.Contracts; + +namespace Agentstration.Web.Tests; + +public sealed partial class ApiClientTests +{ + [TestMethod] + public async Task WorkClientMapsPublicContractToConsoleModel() + { + var timestamp = new DateTimeOffset(2026, 8, 1, 10, 0, 0, TimeSpan.Zero); + var response = new WorkTaskOperationsPageResponse( + [new WorkTaskOperationsSummary(Guid.NewGuid(), "personal", "review", Guid.NewGuid(), "Review API", null, WorkTaskStatus.Running, timestamp, timestamp, timestamp, null, "flowrun-1", null, 0, 0, 0, 1, "Work started", null)], + 1, 100, 1); + using var httpClient = new HttpClient(new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(response) })) + { + BaseAddress = new Uri("http://localhost/") + }; + var client = new WorkApiClient(httpClient); + + var items = await client.GetWorkItemsAsync(CancellationToken.None); + + Assert.HasCount(1, items); + Assert.AreEqual("Review API", items[0].Title); + Assert.AreEqual("Running", items[0].Status); + Assert.AreEqual("personal", items[0].Owner); + } + + [TestMethod] + public async Task WorkClientRespondsToTaskScopedPendingActionWithoutInteractionToken() + { + var taskId = Guid.NewGuid(); + var actionId = Guid.NewGuid(); + var workspaceId = Guid.NewGuid(); + HttpMethod? method = null; + string? path = null; + string? body = null; + var contract = new PendingActionContract(actionId, workspaceId, null, taskId, "run-1", PendingActionKind.ConfirmationRequired, PendingActionStatus.Completed, "Approve", null, [], DateTimeOffset.UtcNow, null, DateTimeOffset.UtcNow, 2); + using var httpClient = new HttpClient(new StubHandler(request => + { + method = request.Method; + path = request.RequestUri!.AbsolutePath; + body = request.Content!.ReadAsStringAsync().GetAwaiter().GetResult(); + return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(contract) }; + })) + { BaseAddress = new Uri("http://localhost/") }; + + var actual = await new WorkApiClient(httpClient).RespondTaskPendingActionAsync(taskId, actionId, new Dictionary { ["confirmed"] = JsonSerializer.SerializeToElement(true) }, default); + + Assert.AreEqual(HttpMethod.Post, method); + Assert.AreEqual($"/api/tasks/{taskId}/pending-actions/{actionId}/respond", path); + StringAssert.Contains(body, "confirmed"); + Assert.AreEqual(actionId, actual.Id); + Assert.IsNull(actual.InteractionId); + } + + [TestMethod] + public async Task WorkClientExposesSafeErrorIdentifier() + { + using var httpClient = new HttpClient(new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.ServiceUnavailable))) + { + BaseAddress = new Uri("http://localhost/") + }; + var client = new WorkApiClient(httpClient); + + var exception = await Assert.ThrowsAsync(() => client.GetWorkItemsAsync(CancellationToken.None)); + + Assert.IsFalse(string.IsNullOrWhiteSpace(exception.ErrorId)); + } + + [TestMethod] + public async Task WorkClientListsWorkplaceWorkspacesThroughUnambiguousApiRoute() + { + string? requestPath = null; + using var httpClient = new HttpClient(new StubHandler(request => + { + requestPath = request.RequestUri?.AbsolutePath; + return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(Array.Empty()) }; + })) + { BaseAddress = new Uri("http://localhost/") }; + var client = new WorkApiClient(httpClient); + + _ = await client.GetWorkspacesAsync(CancellationToken.None); + + Assert.AreEqual("/api/workplace/workspaces", requestPath); + } +} diff --git a/tests/Agentstration.Web.Tests/ApiClientTests.cs b/tests/Agentstration.Console.Client.Tests/ApiClientTests.cs similarity index 84% rename from tests/Agentstration.Web.Tests/ApiClientTests.cs rename to tests/Agentstration.Console.Client.Tests/ApiClientTests.cs index dd4b5281..2f1c8c0f 100644 --- a/tests/Agentstration.Web.Tests/ApiClientTests.cs +++ b/tests/Agentstration.Console.Client.Tests/ApiClientTests.cs @@ -10,23 +10,9 @@ using Agentstration.Runtime.Abstractions; using Agentstration.Runtime.Contracts; using Agentstration.Web.Components; -using Agentstration.Web.Configuration; using Agentstration.Web.Console; -using Agentstration.Web.Features.Flows.Designer; -using Agentstration.Web.FlowDesigner.Backend; -using Agentstration.Web.Security; using Agentstration.Work; using Agentstration.Work.Contracts; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; namespace Agentstration.Web.Tests; @@ -144,14 +130,4 @@ private sealed class StubHttpClientFactory(Func factory) : I public HttpClient CreateClient(string name) => factory(name); } - private sealed class TestHostEnvironment : IHostEnvironment - { - public string EnvironmentName { get; set; } = "Testing"; - - public string ApplicationName { get; set; } = nameof(ApiClientTests); - - public string ContentRootPath { get; set; } = AppContext.BaseDirectory; - - public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); - } } diff --git a/tests/Agentstration.Web.Tests/IdentityAdministrationApiClientTests.cs b/tests/Agentstration.Console.Client.Tests/IdentityAdministrationApiClientTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/IdentityAdministrationApiClientTests.cs rename to tests/Agentstration.Console.Client.Tests/IdentityAdministrationApiClientTests.cs diff --git a/tests/Agentstration.Web.Tests/SourceConsoleApiClientTests.cs b/tests/Agentstration.Console.Client.Tests/SourceConsoleApiClientTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/SourceConsoleApiClientTests.cs rename to tests/Agentstration.Console.Client.Tests/SourceConsoleApiClientTests.cs diff --git a/tests/Agentstration.Console.Components.Tests/Agentstration.Console.Components.Tests.csproj b/tests/Agentstration.Console.Components.Tests/Agentstration.Console.Components.Tests.csproj new file mode 100644 index 00000000..33199d16 --- /dev/null +++ b/tests/Agentstration.Console.Components.Tests/Agentstration.Console.Components.Tests.csproj @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Agentstration.Web.Tests/CleanupConsoleTests.cs b/tests/Agentstration.Console.Components.Tests/CleanupConsoleTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/CleanupConsoleTests.cs rename to tests/Agentstration.Console.Components.Tests/CleanupConsoleTests.cs diff --git a/tests/Agentstration.Web.Tests/DashboardTests.cs b/tests/Agentstration.Console.Components.Tests/DashboardTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/DashboardTests.cs rename to tests/Agentstration.Console.Components.Tests/DashboardTests.cs diff --git a/tests/Agentstration.Web.Tests/EntryAdministrationComponentTests.cs b/tests/Agentstration.Console.Components.Tests/EntryAdministrationComponentTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/EntryAdministrationComponentTests.cs rename to tests/Agentstration.Console.Components.Tests/EntryAdministrationComponentTests.cs diff --git a/tests/Agentstration.Web.Tests/ExtensionLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/ExtensionLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ExtensionLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/ExtensionLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/ExtensionOptionsEditorModelTests.cs b/tests/Agentstration.Console.Components.Tests/ExtensionOptionsEditorModelTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ExtensionOptionsEditorModelTests.cs rename to tests/Agentstration.Console.Components.Tests/ExtensionOptionsEditorModelTests.cs diff --git a/tests/Agentstration.Web.Tests/ExtensionsConsoleTests.cs b/tests/Agentstration.Console.Components.Tests/ExtensionsConsoleTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ExtensionsConsoleTests.cs rename to tests/Agentstration.Console.Components.Tests/ExtensionsConsoleTests.cs diff --git a/tests/Agentstration.Web.Tests/FlowDetailsDesignerTests.cs b/tests/Agentstration.Console.Components.Tests/FlowDetailsDesignerTests.cs similarity index 97% rename from tests/Agentstration.Web.Tests/FlowDetailsDesignerTests.cs rename to tests/Agentstration.Console.Components.Tests/FlowDetailsDesignerTests.cs index 2bd97b65..2b500585 100644 --- a/tests/Agentstration.Web.Tests/FlowDetailsDesignerTests.cs +++ b/tests/Agentstration.Console.Components.Tests/FlowDetailsDesignerTests.cs @@ -8,9 +8,8 @@ using Agentstration.Web.Components.Models; using Agentstration.Web.Components.Pages; using Agentstration.Web.Console; -using Agentstration.Web.Security; using Bunit; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Connections.Client; using Microsoft.Extensions.DependencyInjection; namespace Agentstration.Web.Tests; @@ -124,7 +123,7 @@ public void FlowRunCausalityTabShowsVersionsAttemptsAndGovernanceDeepLink() new(run.Id, FlowInvocationOrigin.Trigger, FlowRunTrigger.Event, "news-watcher", "news-item-42", "news-42", Guid.NewGuid().ToString("D")), [node], 1, null); context.Services.AddSingleton(new FlowClientStub(run: run, causality: causality)); - context.Services.AddSingleton(new ConsoleRealtimeSession(new HttpContextAccessor(), new UninitializedRequestContext())); + context.Services.AddSingleton(NoOpRealtimeConfigurator.Instance); context.Services.AddSingleton(TimeProvider.System); var strings = context.Services.GetRequiredService>(); @@ -148,7 +147,7 @@ public void MissingFlowRunRendersNotFoundStateInsteadOfThrowing() using var context = new BunitContext(); context.Services.AddLocalization(options => options.ResourcesPath = "Resources"); context.Services.AddSingleton(new FlowClientStub()); - context.Services.AddSingleton(new ConsoleRealtimeSession(new HttpContextAccessor(), new UninitializedRequestContext())); + context.Services.AddSingleton(NoOpRealtimeConfigurator.Instance); context.Services.AddSingleton(TimeProvider.System); var strings = context.Services.GetRequiredService>(); @@ -230,9 +229,12 @@ private sealed class ManagementClientStub : IManagementApiClient public Task GetSummaryAsync(CancellationToken cancellationToken) => throw new NotSupportedException(); } - private sealed class UninitializedRequestContext : ICurrentRequestContext + private sealed class NoOpRealtimeConfigurator : IConsoleRealtimeConnectionConfigurator { - public bool IsInitialized => false; - public RequestContext Current => throw new InvalidOperationException("The request context is not initialized."); + public static NoOpRealtimeConfigurator Instance { get; } = new(); + + public void Configure(Uri hubUri, HttpConnectionOptions options) + { + } } } diff --git a/tests/Agentstration.Web.Tests/FlowRunEventBufferTests.cs b/tests/Agentstration.Console.Components.Tests/FlowRunEventBufferTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/FlowRunEventBufferTests.cs rename to tests/Agentstration.Console.Components.Tests/FlowRunEventBufferTests.cs diff --git a/tests/Agentstration.Web.Tests/FlowRunTimelineTests.cs b/tests/Agentstration.Console.Components.Tests/FlowRunTimelineTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/FlowRunTimelineTests.cs rename to tests/Agentstration.Console.Components.Tests/FlowRunTimelineTests.cs diff --git a/tests/Agentstration.Web.Tests/LocalizationResidualTests.cs b/tests/Agentstration.Console.Components.Tests/LocalizationResidualTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/LocalizationResidualTests.cs rename to tests/Agentstration.Console.Components.Tests/LocalizationResidualTests.cs diff --git a/tests/Agentstration.Web.Tests/ManagementLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/ManagementLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ManagementLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/ManagementLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/ModelProfilePickerTests.cs b/tests/Agentstration.Console.Components.Tests/ModelProfilePickerTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ModelProfilePickerTests.cs rename to tests/Agentstration.Console.Components.Tests/ModelProfilePickerTests.cs diff --git a/tests/Agentstration.Web.Tests/ModelProviderNavigationTests.cs b/tests/Agentstration.Console.Components.Tests/ModelProviderNavigationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ModelProviderNavigationTests.cs rename to tests/Agentstration.Console.Components.Tests/ModelProviderNavigationTests.cs diff --git a/tests/Agentstration.Web.Tests/NamespacedAgentDetailsTests.cs b/tests/Agentstration.Console.Components.Tests/NamespacedAgentDetailsTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/NamespacedAgentDetailsTests.cs rename to tests/Agentstration.Console.Components.Tests/NamespacedAgentDetailsTests.cs diff --git a/tests/Agentstration.Web.Tests/OrchestrationConsoleComponentTests.cs b/tests/Agentstration.Console.Components.Tests/OrchestrationConsoleComponentTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/OrchestrationConsoleComponentTests.cs rename to tests/Agentstration.Console.Components.Tests/OrchestrationConsoleComponentTests.cs diff --git a/tests/Agentstration.Web.Tests/OrchestrationEditorModelTests.cs b/tests/Agentstration.Console.Components.Tests/OrchestrationEditorModelTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/OrchestrationEditorModelTests.cs rename to tests/Agentstration.Console.Components.Tests/OrchestrationEditorModelTests.cs diff --git a/tests/Agentstration.Web.Tests/OrganizationLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/OrganizationLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/OrganizationLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/OrganizationLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/OverviewRenderingTests.cs b/tests/Agentstration.Console.Components.Tests/OverviewRenderingTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/OverviewRenderingTests.cs rename to tests/Agentstration.Console.Components.Tests/OverviewRenderingTests.cs diff --git a/tests/Agentstration.Web.Tests/PackAgentDefinitionFormTests.cs b/tests/Agentstration.Console.Components.Tests/PackAgentDefinitionFormTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/PackAgentDefinitionFormTests.cs rename to tests/Agentstration.Console.Components.Tests/PackAgentDefinitionFormTests.cs diff --git a/tests/Agentstration.Web.Tests/PackLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/PackLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/PackLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/PackLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/PacksComponentTests.cs b/tests/Agentstration.Console.Components.Tests/PacksComponentTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/PacksComponentTests.cs rename to tests/Agentstration.Console.Components.Tests/PacksComponentTests.cs diff --git a/tests/Agentstration.Web.Tests/ResourceScopeFormFieldTests.cs b/tests/Agentstration.Console.Components.Tests/ResourceScopeFormFieldTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ResourceScopeFormFieldTests.cs rename to tests/Agentstration.Console.Components.Tests/ResourceScopeFormFieldTests.cs diff --git a/tests/Agentstration.Web.Tests/ResourceScopesComponentTests.cs b/tests/Agentstration.Console.Components.Tests/ResourceScopesComponentTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ResourceScopesComponentTests.cs rename to tests/Agentstration.Console.Components.Tests/ResourceScopesComponentTests.cs diff --git a/tests/Agentstration.Web.Tests/ResourceScopesLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/ResourceScopesLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ResourceScopesLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/ResourceScopesLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/RunEventsLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/RunEventsLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/RunEventsLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/RunEventsLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/RuntimeOperationsLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/RuntimeOperationsLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/RuntimeOperationsLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/RuntimeOperationsLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/SecretLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/SecretLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/SecretLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/SecretLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/SharedComponentsLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/SharedComponentsLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/SharedComponentsLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/SharedComponentsLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/SourceConsoleLocalePresentationTests.cs b/tests/Agentstration.Console.Components.Tests/SourceConsoleLocalePresentationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/SourceConsoleLocalePresentationTests.cs rename to tests/Agentstration.Console.Components.Tests/SourceConsoleLocalePresentationTests.cs diff --git a/tests/Agentstration.Web.Tests/SourceProviderConsoleTests.cs b/tests/Agentstration.Console.Components.Tests/SourceProviderConsoleTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/SourceProviderConsoleTests.cs rename to tests/Agentstration.Console.Components.Tests/SourceProviderConsoleTests.cs diff --git a/tests/Agentstration.Web.Tests/SourceRegistriesLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/SourceRegistriesLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/SourceRegistriesLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/SourceRegistriesLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/SourceRegistryConsoleTests.cs b/tests/Agentstration.Console.Components.Tests/SourceRegistryConsoleTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/SourceRegistryConsoleTests.cs rename to tests/Agentstration.Console.Components.Tests/SourceRegistryConsoleTests.cs diff --git a/tests/Agentstration.Web.Tests/SourcesLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/SourcesLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/SourcesLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/SourcesLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/TaskDetailsLocalizationTests.cs b/tests/Agentstration.Console.Components.Tests/TaskDetailsLocalizationTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/TaskDetailsLocalizationTests.cs rename to tests/Agentstration.Console.Components.Tests/TaskDetailsLocalizationTests.cs diff --git a/tests/Agentstration.Web.Tests/TestCultureScope.cs b/tests/Agentstration.Console.Components.Tests/TestCultureScope.cs similarity index 100% rename from tests/Agentstration.Web.Tests/TestCultureScope.cs rename to tests/Agentstration.Console.Components.Tests/TestCultureScope.cs diff --git a/tests/Agentstration.Web.Tests/ToolCatalogTests.cs b/tests/Agentstration.Console.Components.Tests/ToolCatalogTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ToolCatalogTests.cs rename to tests/Agentstration.Console.Components.Tests/ToolCatalogTests.cs diff --git a/tests/Agentstration.Web.Tests/ToolDefinitionConsoleTests.cs b/tests/Agentstration.Console.Components.Tests/ToolDefinitionConsoleTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ToolDefinitionConsoleTests.cs rename to tests/Agentstration.Console.Components.Tests/ToolDefinitionConsoleTests.cs diff --git a/tests/Agentstration.Web.Tests/ToolGovernanceAuditConsoleTests.cs b/tests/Agentstration.Console.Components.Tests/ToolGovernanceAuditConsoleTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ToolGovernanceAuditConsoleTests.cs rename to tests/Agentstration.Console.Components.Tests/ToolGovernanceAuditConsoleTests.cs diff --git a/tests/Agentstration.Web.Tests/ToolProviderEditorTests.cs b/tests/Agentstration.Console.Components.Tests/ToolProviderEditorTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/ToolProviderEditorTests.cs rename to tests/Agentstration.Console.Components.Tests/ToolProviderEditorTests.cs diff --git a/tests/Agentstration.Web.Tests/TriggerUxTests.cs b/tests/Agentstration.Console.Components.Tests/TriggerUxTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/TriggerUxTests.cs rename to tests/Agentstration.Console.Components.Tests/TriggerUxTests.cs diff --git a/tests/Agentstration.Web.Tests/WorkOperationsComponentTests.cs b/tests/Agentstration.Console.Components.Tests/WorkOperationsComponentTests.cs similarity index 100% rename from tests/Agentstration.Web.Tests/WorkOperationsComponentTests.cs rename to tests/Agentstration.Console.Components.Tests/WorkOperationsComponentTests.cs diff --git a/tests/Agentstration.Management.Sources.Tests/Agentstration.Management.Sources.Tests.csproj b/tests/Agentstration.Management.Sources.Tests/Agentstration.Management.Sources.Tests.csproj index 94aec0d4..cdbf17d2 100644 --- a/tests/Agentstration.Management.Sources.Tests/Agentstration.Management.Sources.Tests.csproj +++ b/tests/Agentstration.Management.Sources.Tests/Agentstration.Management.Sources.Tests.csproj @@ -1,6 +1,5 @@ - @@ -9,9 +8,5 @@ - - - - diff --git a/tests/Agentstration.Runtime.Tests/Agentstration.Runtime.Tests.csproj b/tests/Agentstration.Runtime.Tests/Agentstration.Runtime.Tests.csproj index 59a3a436..94bcb1a9 100644 --- a/tests/Agentstration.Runtime.Tests/Agentstration.Runtime.Tests.csproj +++ b/tests/Agentstration.Runtime.Tests/Agentstration.Runtime.Tests.csproj @@ -1,6 +1,7 @@ - + + @@ -9,10 +10,6 @@ - - - - diff --git a/tests/Agentstration.Web.Tests/Agentstration.Web.Tests.csproj b/tests/Agentstration.Web.Tests/Agentstration.Web.Tests.csproj index e429393c..e8ee19f7 100644 --- a/tests/Agentstration.Web.Tests/Agentstration.Web.Tests.csproj +++ b/tests/Agentstration.Web.Tests/Agentstration.Web.Tests.csproj @@ -1,20 +1,14 @@ - - - - - - - + + + - - diff --git a/tests/Agentstration.Web.Tests/ApiClientTests.WorkplaceAndAuth.cs b/tests/Agentstration.Web.Tests/ApiClientTests.WorkplaceAndAuth.cs deleted file mode 100644 index 4038602c..00000000 --- a/tests/Agentstration.Web.Tests/ApiClientTests.WorkplaceAndAuth.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System.Net; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Text.Json; -using Agentstration.Flow; -using Agentstration.Flow.Contracts; -using Agentstration.Management.Abstractions; -using Agentstration.Management.Contracts; -using Agentstration.Resources; -using Agentstration.Runtime.Abstractions; -using Agentstration.Runtime.Contracts; -using Agentstration.Web.Components; -using Agentstration.Web.Configuration; -using Agentstration.Web.Console; -using Agentstration.Web.Features.Flows.Designer; -using Agentstration.Web.FlowDesigner.Backend; -using Agentstration.Web.Security; -using Agentstration.Work; -using Agentstration.Work.Contracts; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; - -namespace Agentstration.Web.Tests; - -public sealed partial class ApiClientTests -{ - [TestMethod] - public void OidcApisPreferBearerButAcceptTheTrustedConsoleSession() - { - var services = new ServiceCollection(); - var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary - { - ["Agentstration:Authentication:Mode"] = ApiAuthenticationOptions.Oidc, - ["Agentstration:Authentication:Authority"] = "https://identity.example/", - ["Agentstration:Authentication:Audience"] = "agentstration-api", - ["Agentstration:Authentication:ClientId"] = "agentstration-console" - }).Build(); - services.AddLogging(); - services.AddAgentstrationApi(configuration, new TestHostEnvironment()); - using var provider = services.BuildServiceProvider(); - var selector = provider.GetRequiredService>() - .Get(AgentstrationAuthenticationDefaults.PolicyScheme).ForwardDefaultSelector; - Assert.IsNotNull(selector); - - var api = new DefaultHttpContext(); - api.Request.Path = "/api/agents"; - Assert.AreEqual(JwtBearerDefaults.AuthenticationScheme, selector(api)); - - api.Request.Headers.Cookie = $"{AgentstrationAuthenticationDefaults.ApplicationCookie}=session"; - Assert.AreEqual(IdentityConstants.ApplicationScheme, selector(api)); - - api.Request.Headers.Authorization = "Bearer access-token"; - Assert.AreEqual(JwtBearerDefaults.AuthenticationScheme, selector(api)); - } - - [TestMethod] - public async Task CookieAuthenticationReturnsStatusCodeInsteadOfHtmlRedirectForHubs() - { - var services = new ServiceCollection(); - var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary - { - ["Agentstration:Authentication:Mode"] = ApiAuthenticationOptions.Local - }).Build(); - services.AddLogging(); - services.AddAgentstrationApi(configuration, new TestHostEnvironment()); - using var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>() - .Get(IdentityConstants.ApplicationScheme); - var httpContext = new DefaultHttpContext(); - httpContext.Request.Path = "/hubs/flow-runs/negotiate"; - var redirect = new RedirectContext( - httpContext, - new AuthenticationScheme(IdentityConstants.ApplicationScheme, null, typeof(CookieAuthenticationHandler)), - options, - new AuthenticationProperties(), - "/login"); - - await options.Events.OnRedirectToLogin(redirect); - - Assert.AreEqual(StatusCodes.Status401Unauthorized, httpContext.Response.StatusCode); - Assert.IsFalse(httpContext.Response.Headers.ContainsKey("Location")); - } - - [TestMethod] - public void ConsoleUsesCanonicalHttpClients() - { - var services = new ServiceCollection(); - var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary - { - ["Agentstration:ManagementApi:BaseAddress"] = "http://localhost:5080/", - ["Agentstration:RuntimeApi:BaseAddress"] = "http://localhost:5080/" - }).Build(); - services.AddLogging(); - services.AddAgentstrationWebConsole(configuration, new TestHostEnvironment()); - using var provider = services.BuildServiceProvider(); - using var scope = provider.CreateScope(); - - Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); - Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); - Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); - Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); - Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); - Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); - Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); - Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); - } - - [TestMethod] - public async Task WorkClientMapsPublicContractToConsoleModel() - { - var timestamp = new DateTimeOffset(2026, 8, 1, 10, 0, 0, TimeSpan.Zero); - var response = new WorkTaskOperationsPageResponse( - [new WorkTaskOperationsSummary(Guid.NewGuid(), "personal", "review", Guid.NewGuid(), "Review API", null, WorkTaskStatus.Running, timestamp, timestamp, timestamp, null, "flowrun-1", null, 0, 0, 0, 1, "Work started", null)], - 1, 100, 1); - using var httpClient = new HttpClient(new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(response) })) - { - BaseAddress = new Uri("http://localhost/") - }; - var client = new WorkApiClient(httpClient); - - var items = await client.GetWorkItemsAsync(CancellationToken.None); - - Assert.HasCount(1, items); - Assert.AreEqual("Review API", items[0].Title); - Assert.AreEqual("Running", items[0].Status); - Assert.AreEqual("personal", items[0].Owner); - } - - [TestMethod] - public async Task WorkClientRespondsToTaskScopedPendingActionWithoutInteractionToken() - { - var taskId = Guid.NewGuid(); - var actionId = Guid.NewGuid(); - var workspaceId = Guid.NewGuid(); - HttpMethod? method = null; - string? path = null; - string? body = null; - var contract = new PendingActionContract(actionId, workspaceId, null, taskId, "run-1", PendingActionKind.ConfirmationRequired, PendingActionStatus.Completed, "Approve", null, [], DateTimeOffset.UtcNow, null, DateTimeOffset.UtcNow, 2); - using var httpClient = new HttpClient(new StubHandler(request => - { - method = request.Method; - path = request.RequestUri!.AbsolutePath; - body = request.Content!.ReadAsStringAsync().GetAwaiter().GetResult(); - return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(contract) }; - })) - { BaseAddress = new Uri("http://localhost/") }; - - var actual = await new WorkApiClient(httpClient).RespondTaskPendingActionAsync(taskId, actionId, new Dictionary { ["confirmed"] = JsonSerializer.SerializeToElement(true) }, default); - - Assert.AreEqual(HttpMethod.Post, method); - Assert.AreEqual($"/api/tasks/{taskId}/pending-actions/{actionId}/respond", path); - StringAssert.Contains(body, "confirmed"); - Assert.AreEqual(actionId, actual.Id); - Assert.IsNull(actual.InteractionId); - } - - [TestMethod] - public async Task WorkClientExposesSafeErrorIdentifier() - { - using var httpClient = new HttpClient(new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.ServiceUnavailable))) - { - BaseAddress = new Uri("http://localhost/") - }; - var client = new WorkApiClient(httpClient); - - var exception = await Assert.ThrowsAsync(() => client.GetWorkItemsAsync(CancellationToken.None)); - - Assert.IsFalse(string.IsNullOrWhiteSpace(exception.ErrorId)); - } - - [TestMethod] - public async Task WorkClientListsWorkplaceWorkspacesThroughUnambiguousApiRoute() - { - string? requestPath = null; - using var httpClient = new HttpClient(new StubHandler(request => - { - requestPath = request.RequestUri?.AbsolutePath; - return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(Array.Empty()) }; - })) - { BaseAddress = new Uri("http://localhost/") }; - var client = new WorkApiClient(httpClient); - - _ = await client.GetWorkspacesAsync(CancellationToken.None); - - Assert.AreEqual("/api/workplace/workspaces", requestPath); - } - -} - diff --git a/tests/Agentstration.Web.Tests/FlowDesignerBackendTests.cs b/tests/Agentstration.Web.Tests/FlowDesignerBackendTests.cs new file mode 100644 index 00000000..e3ada391 --- /dev/null +++ b/tests/Agentstration.Web.Tests/FlowDesignerBackendTests.cs @@ -0,0 +1,126 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Agentstration.Flow; +using Agentstration.Flow.Application; +using Agentstration.Flow.Contracts; +using Agentstration.Resources; +using Agentstration.Web.Console; +using Agentstration.Web.Features.Flows.Designer; +using Agentstration.Web.FlowDesigner.Backend; + +namespace Agentstration.Web.Tests; + +[TestClass] +public sealed class FlowDesignerBackendTests +{ + private static readonly WorkspaceId TestWorkspaceId = new(Guid.Parse("11111111-1111-1111-1111-111111111111")); + + [TestMethod] + public async Task MaterializesDraftFromActivePublishedVersion() + { + var now = new DateTimeOffset(2026, 8, 6, 12, 0, 0, TimeSpan.Zero); + var flowId = new FlowId("universal-router"); + var definition = new DirectFlowDefinition(new FlowTargetReference(FlowTargetKind.Agent, "agent-id")); + var flow = new FlowResponse(flowId.Value, flowId.Value, null, "1.0.0", true, "1.0.0", definition, new Dictionary(), now, now); + var draft = new FlowDraftResponse(new FlowDraft + { + WorkspaceId = TestWorkspaceId, + Id = "draft-universal-router", + FlowId = flowId, + DisplayName = "Universal router", + Definition = new FlowGraphDefinition { EntryStep = "input", Steps = [new InputFlowStepDefinition { Name = "input" }], Transitions = [] }, + CreatedAt = now, + UpdatedAt = now + }, "\"draft-etag\""); + var requests = new List(); + using var httpClient = new HttpClient(new StubHandler(request => + { + requests.Add($"{request.Method} {request.RequestUri!.AbsolutePath}"); + if (request.Method == HttpMethod.Get && request.RequestUri.AbsolutePath.EndsWith("/draft", StringComparison.Ordinal)) + return new HttpResponseMessage(HttpStatusCode.NotFound) { Content = JsonContent.Create(new { title = "flow_draft_not_found", status = 404 }) }; + if (request.Method == HttpMethod.Get && request.RequestUri.AbsolutePath.EndsWith("/draft/source", StringComparison.Ordinal)) + return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(new FlowSourceResponse("entryStep: input", "yaml", 1)) }; + if (request.Method == HttpMethod.Get) + return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(flow) }; + return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(draft) }; + })) + { BaseAddress = new Uri("http://localhost/") }; + + var actual = await new FlowDesignerBackend(new FlowApiClient(httpClient)).LoadAsync(new(ResourceNamespace.Default, flowId.Value), default); + + Assert.AreEqual(flowId, actual.Resource.FlowId); + CollectionAssert.AreEqual(new[] + { + "GET /api/flows/universal-router/draft", + "GET /api/flows/universal-router", + "POST /api/flows/universal-router/versions/1.0.0/draft", + "GET /api/flows/universal-router/draft/source" + }, requests); + } + + [TestMethod] + public async Task LoadsNamespacedPublishedGraphWithoutDraftCallsAndRejectsMutations() + { + var now = new DateTimeOffset(2026, 8, 15, 12, 0, 0, TimeSpan.Zero); + var @namespace = new ResourceNamespace("pack.sample"); + var graph = new FlowGraphDefinition { EntryStep = "input", Steps = [new InputFlowStepDefinition { Name = "input" }], Transitions = [] }; + var definition = new DirectFlowDefinition(new FlowTargetReference(FlowTargetKind.Agent, "agent-id")); + var flow = new FlowResponse("sample", "Pack sample", null, "1.2.0", true, "1.2.0", definition, new Dictionary(), now, now) { Namespace = @namespace }; + var version = new FlowVersionResponse("sample", "1.2.0", null, definition, new Dictionary(), now, graph) { Namespace = @namespace }; + var requests = new List(); + using var httpClient = new HttpClient(new StubHandler(request => + { + requests.Add($"{request.Method} {request.RequestUri!.AbsolutePath}"); + return request.RequestUri.AbsolutePath.EndsWith("/versions/1.2.0", StringComparison.Ordinal) + ? new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(version) } + : new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(flow) }; + })) + { BaseAddress = new Uri("http://localhost/") }; + var backend = new FlowDesignerBackend(new FlowApiClient(httpClient)); + var target = new FlowDesignerTarget(@namespace, "sample"); + + var loaded = await backend.LoadAsync(target, default); + + Assert.AreEqual("1.2.0", loaded.PublishedVersion); + StringAssert.Contains(loaded.Source, "entryStep: input"); + CollectionAssert.AreEqual(new[] + { + "GET /api/namespaces/pack.sample/flows/sample", + "GET /api/namespaces/pack.sample/flows/sample/versions/1.2.0" + }, requests); + await Assert.ThrowsExactlyAsync(() => backend.SaveDraftAsync(target, new("Sample", null, null, graph), string.Empty, default)); + await Assert.ThrowsExactlyAsync(() => backend.ReplaceSourceAsync(target, new("entryStep: input"), string.Empty, default)); + await Assert.ThrowsExactlyAsync(() => backend.PublishAsync(target, new("1.3.0"), default)); + using var input = JsonDocument.Parse("{}"); + await Assert.ThrowsExactlyAsync(() => backend.RunDraftAsync(target, new(input.RootElement.Clone()), default)); + Assert.HasCount(2, requests); + } + + [TestMethod] + public async Task ReportsLegacyNamespacedVersionWithoutGraph() + { + var now = new DateTimeOffset(2026, 8, 15, 12, 0, 0, TimeSpan.Zero); + var @namespace = new ResourceNamespace("pack.legacy"); + var definition = new DirectFlowDefinition(new FlowTargetReference(FlowTargetKind.Agent, "agent-id")); + var flow = new FlowResponse("legacy", "Legacy", null, "1.0.0", true, "1.0.0", definition, new Dictionary(), now, now) { Namespace = @namespace }; + var version = new FlowVersionResponse("legacy", "1.0.0", null, definition, new Dictionary(), now) { Namespace = @namespace }; + using var httpClient = new HttpClient(new StubHandler(request => request.RequestUri!.AbsolutePath.EndsWith("/versions/1.0.0", StringComparison.Ordinal) + ? new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(version) } + : new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(flow) })) + { BaseAddress = new Uri("http://localhost/") }; + + var exception = await Assert.ThrowsExactlyAsync(() => new FlowDesignerBackend(new FlowApiClient(httpClient)).LoadAsync(new(@namespace, "legacy"), default)); + + StringAssert.Contains(exception.Message, "legacy Flow version without a Graph"); + } + + private sealed class StubHandler(Func responseFactory) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(responseFactory(request)); + } + } +} diff --git a/tests/Agentstration.Web.Tests/WebConsoleRegistrationTests.cs b/tests/Agentstration.Web.Tests/WebConsoleRegistrationTests.cs new file mode 100644 index 00000000..64e10b65 --- /dev/null +++ b/tests/Agentstration.Web.Tests/WebConsoleRegistrationTests.cs @@ -0,0 +1,46 @@ +using Agentstration.Web; +using Agentstration.Web.Components; +using Agentstration.Web.Configuration; +using Agentstration.Web.Console; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; + +namespace Agentstration.Web.Tests; + +[TestClass] +public sealed class WebConsoleRegistrationTests +{ + [TestMethod] + public void ConsoleUsesCanonicalHttpClients() + { + var services = new ServiceCollection(); + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["Agentstration:ManagementApi:BaseAddress"] = "http://localhost:5080/", + ["Agentstration:RuntimeApi:BaseAddress"] = "http://localhost:5080/" + }).Build(); + services.AddLogging(); + services.AddAgentstrationWebConsole(configuration, new TestHostEnvironment()); + using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + + Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); + Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); + Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); + Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); + Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); + Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); + Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); + Assert.IsInstanceOfType(scope.ServiceProvider.GetRequiredService()); + } + + private sealed class TestHostEnvironment : IHostEnvironment + { + public string EnvironmentName { get; set; } = "Testing"; + public string ApplicationName { get; set; } = nameof(WebConsoleRegistrationTests); + public string ContentRootPath { get; set; } = AppContext.BaseDirectory; + public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); + } +} diff --git a/tests/Agentstration.Work.Api.Tests/Agentstration.Work.Api.Tests.csproj b/tests/Agentstration.Work.Api.Tests/Agentstration.Work.Api.Tests.csproj deleted file mode 100644 index 0d504157..00000000 --- a/tests/Agentstration.Work.Api.Tests/Agentstration.Work.Api.Tests.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - -