Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .claude/skills/create-domain/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ internal record TestRunGroup : DomainEntity<ITestRunGroup>, ITestRunGroup
}
```

### Validation helpers (from `Proxytrace.Common.Validation`)
### Validation helpers (from `Nordstein.Core.Common.Validation`)

```csharp
yield return Validation.NotNullOrWhiteSpace(Name); // note capital S
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ substitute in the container instead (see *Injecting fakes* below).
| `Proxytrace.Application.Tests` | Application services end-to-end (e.g. `TestRunnerService`) with faked infrastructure | `BaseTest<Module>` |
| `Proxytrace.Api.Tests` | HTTP controllers / routing | `BaseTest<Module>` |
| `Proxytrace.Infrastructure.Tests` | `ModelClient` and external integration wrappers | `BaseTest<Module>` |
| `Proxytrace.Serialization.Tests`, `Proxytrace.Common.Tests`, `Proxytrace.Proxy.Tests`, `Proxytrace.Messaging.Tests`, `Proxytrace.Licensing.Tests` | Their respective layers | `BaseTest<Module>` |
| `Proxytrace.Serialization.Tests`, `Nordstein.Core.Common.Tests`, `Proxytrace.Proxy.Tests`, `Proxytrace.Messaging.Tests`, `Proxytrace.Licensing.Tests` | Their respective layers | `BaseTest<Module>` |

Each test project ships **one `Module : Autofac.Module`** that wires the layer under test
plus in-memory storage and the standard infrastructure stubs. This per-project module *is*
Expand All @@ -53,7 +53,7 @@ the shared baseline — there are no other shared fixtures. See *The per-project

## The base test harness

All tests extend `BaseTest<TModule>` from `Proxytrace.Testing`:
All tests extend `BaseTest<TModule>` from `Nordstein.Core.Testing`:

```csharp
[TestClass]
Expand Down Expand Up @@ -84,7 +84,7 @@ public sealed class MyTests : BaseTest<Module> // or DomainTest<Module>
### How the harness works (and why it's stateless)

- `GetServices(action)` builds a **brand-new Autofac container every call**: it registers
`Proxytrace.Testing.Module`, then your `TModule`, then runs `ConfigureContainer`, then
`Nordstein.Core.Testing.Module`, then your `TModule`, then runs `ConfigureContainer`, then
your per-call `action`. Each container has its **own isolated in-memory database**.
- The container is recorded in `TestContext.Properties["Containers"]` and disposed in
`[TestCleanup]`. You never manage container lifetime yourself.
Expand Down
4 changes: 3 additions & 1 deletion .github/actions/detect-changes/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ runs:
all=true
fi

backend_re='^Proxytrace\.|^Proxytrace\.sln$|^Directory\.Build\.props$|^coverage\.runsettings$|^dotnet-tools\.json$'
# `^core/` is the Nordstein.Core staging area: the product consumes it as a project or
# package reference, so a change there is a backend change like any other.
backend_re='^Proxytrace\.|^Proxytrace\.sln$|^core/|^Directory\.Build\.props$|^Directory\.Build\.targets$|^nuget\.config$|^coverage\.runsettings$|^dotnet-tools\.json$'
frontend_re='^frontend/'
# The all-in-one image bundles both halves of the app plus its own packaging.
# Deliberately NOT including `^manual/`: the image build does compile the manual,
Expand Down
65 changes: 62 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,80 @@ jobs:
cache-dependency-path: |
**/*.csproj

# Two solutions: Nordstein.Core is a separate, product-agnostic solution that Proxytrace
# consumes (see docs/code-reuse.md). It is built and tested first because the product
# depends on it, and separately because it must keep standing on its own — the moment it
# only compiles as part of Proxytrace.sln, it is no longer extractable.
- name: Restore
run: dotnet restore Proxytrace.sln --nologo
run: |
dotnet restore core/Nordstein.Core.sln --nologo
dotnet restore Proxytrace.sln --nologo

- name: Build
run: dotnet build Proxytrace.sln --nologo --no-restore
run: |
dotnet build core/Nordstein.Core.sln --nologo --no-restore
dotnet build Proxytrace.sln --nologo --no-restore

# PROXYTRACE_REQUIRE_DOCKER_TESTS turns the container-backed tests (currently the real-Redis
# ingestion transport ones) from skip-if-unavailable into hard failures. They must stay
# skippable locally so `dotnet test` never requires Docker, but a runner that quietly lost its
# container runtime would then drop exactly the coverage those tests exist to guarantee.
- name: Test
run: dotnet test Proxytrace.sln --nologo --verbosity minimal --no-restore
run: |
dotnet test core/Nordstein.Core.sln --nologo --verbosity minimal --no-restore
dotnet test Proxytrace.sln --nologo --verbosity minimal --no-restore
env:
PROXYTRACE_REQUIRE_DOCKER_TESTS: true

# Packs Nordstein.Core and rebuilds the product against the resulting .nupkg files instead of
# the sources. Everyday builds use the project references, which hide two things a consumer
# would hit: a type that is public in source but never made it into the package's surface, and
# a dependency Core forgot to declare (source mode resolves it through the product's own
# graph). Catching that here is the difference between finding it now and finding it in the
# first repository that has no Proxytrace sources to fall back on.
core-package:
needs: changes
if: ${{ inputs.full || needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup .NET 10 SDK
uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
cache: true
cache-dependency-path: |
**/*.csproj

- name: Pack Nordstein.Core
run: >-
dotnet pack core/Nordstein.Core.sln --nologo -c Release
-p:NordsteinCoreVersion=0.1.0-ci.${{ github.run_number }}
-o core/artifacts

# The local feed is picked up from core/artifacts by Directory.Build.props; it is not
# passed here, because a relative source path resolves per project directory and silently
# only fails once the packages are absent from the global cache.
- name: Build the product against the packages
run: >-
dotnet build Proxytrace.sln --nologo
-p:UseLocalCore=false
-p:NordsteinCoreVersion=0.1.0-ci.${{ github.run_number }}

- name: Upload packages
uses: actions/upload-artifact@v4
with:
name: nordstein-core-packages
path: |
core/artifacts/*.nupkg
core/artifacts/*.snupkg
if-no-files-found: error
retention-days: 7

# Builds the released all-in-one image and boots it, so a container that fails to come up
# (initdb, supervisord, nginx upstream, migrations on a cold volume) is caught here rather
# than after a release tag is pushed. Single-arch: the release build adds arm64.
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ perf/results/

# Prompt-lab transcripts (generated by the prompt-lab skill; real model calls, not build output)
.prompt-lab/

# Locally packed Nordstein.Core packages (see nuget.config)
core/artifacts/
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Detailed guidance lives in [`docs/`](docs/). Read the relevant page **before** w
| Doc | Read before… |
|-----|--------------|
| [`docs/architecture.md`](docs/architecture.md) | Touching project structure, layering, or Autofac DI/modules |
| [`docs/code-reuse.md`](docs/code-reuse.md) | Touching anything under `core/` (Nordstein.Core), or deciding whether new code is product-agnostic |
| [`docs/code-style.md`](docs/code-style.md) | Writing any backend C# — style rules + key conventions |
| [`docs/domain-entities.md`](docs/domain-entities.md) | Adding/changing a domain entity (the five-file pattern, FK conventions, factory delegates) |
| [`docs/validation.md`](docs/validation.md) | Adding domain validation rules |
Expand Down Expand Up @@ -61,7 +62,7 @@ Detailed guidance lives in [`docs/`](docs/). Read the relevant page **before** w
- **e2e / perf** — never as a routine check. Run them only when the change is in that flow or the
user asks; both boot Docker stacks and take many minutes.
- **Run the full suite** (`dotnet test Proxytrace.sln`) only when the change is genuinely
cross-cutting — `Proxytrace.Common`, `Proxytrace.Testing`, DI/module wiring, a shared interface
cross-cutting — `Nordstein.Core.Common`, `Nordstein.Core.Testing`, DI/module wiring, a shared interface
signature, a package bump — or when cutting a release. Say which scope you ran and why, so a
narrow run is never mistaken for a full one.
- **Internationalization** — the UI is multilingual (English is the source). Every user-facing
Expand All @@ -75,7 +76,7 @@ Detailed guidance lives in [`docs/`](docs/). Read the relevant page **before** w
dedup, title/body quality, and labels — then carry on with your task.
- **Nullable suppression** — suppressing nullable warnings with `!` is strictly forbidden everywhere.
There is exactly **one** sanctioned exception, and it is not extensible: `Validation.Success` in
[`Proxytrace.Common/Validation/Validation.cs`](Proxytrace.Common/Validation/Validation.cs). The BCL
[`core/Nordstein.Core.Common/Validation/Validation.cs`](core/Nordstein.Core.Common/Validation/Validation.cs). The BCL
defines validation success as a `null` `ValidationResult` while declaring
`IValidatableObject.Validate` to return a **non-nullable** element type, so the framework demands a
value it defines as null through a signature we cannot change. That single line is documented in
Expand Down
47 changes: 47 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,51 @@
displayed verbatim in the UI and sent to the license server. -->
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>

<!-- ── Nordstein.Core consumption ────────────────────────────────────────────────
The reusable foundation (Nordstein.Core.Common, Nordstein.Core.Testing) is
product-agnostic code that is destined for its own repository and its own NuGet
packages. It currently lives under core/ in this repository as a staging area, so
the split can be completed with a `git subtree split` on the core/ prefix without
losing history.

A project consumes it by declaring an item rather than a reference:

<ItemGroup>
<NordsteinCoreReference Include="Nordstein.Core.Common" />
</ItemGroup>

Directory.Build.targets turns that into either a ProjectReference (source mode) or a
PackageReference (package mode). Source mode is what you want while developing: F12
navigates into Core, one build covers both halves, and a Core edit is picked up
without a pack/restore round trip. Without it, a cross-repository change becomes
"edit, pack, bump, restore, retest" and nobody makes small Core improvements any more.

Mode is chosen automatically by whether the Core sources are present, and can be
forced either way:

dotnet build -p:UseLocalCore=false -p:NordsteinCoreVersion=0.1.0-dev

Once Core moves out, point NordsteinCorePath at the sibling checkout
(../Core/) — nothing else changes. -->
<PropertyGroup>
<NordsteinCorePath Condition="'$(NordsteinCorePath)' == ''">$(MSBuildThisFileDirectory)core/</NordsteinCorePath>
<UseLocalCore Condition="'$(UseLocalCore)' == '' AND Exists('$(NordsteinCorePath)Nordstein.Core.sln')">true</UseLocalCore>
<UseLocalCore Condition="'$(UseLocalCore)' == ''">false</UseLocalCore>
<!-- Only consulted in package mode. Kept in one place so a Core bump is a one-line diff. -->
<NordsteinCoreVersion Condition="'$(NordsteinCoreVersion)' == ''">0.1.0-dev</NordsteinCoreVersion>
</PropertyGroup>

<!-- Package mode before Core is published anywhere restores from a locally packed feed. The
path is built from $(MSBuildThisFileDirectory) so it is absolute: NuGet resolves a
relative RestoreAdditionalProjectSources against each *project* directory, not the
repository root, so passing `core/artifacts` on the command line asks for
`Proxytrace.Domain/core/artifacts` and fails with NU1301 — and only once the packages are
missing from the global cache, which makes it look like it works right up until CI.

Gated on the directory existing so a real package-mode build against a published feed is
unaffected. -->
<PropertyGroup Condition="'$(UseLocalCore)' != 'true' AND Exists('$(MSBuildThisFileDirectory)core/artifacts')">
<RestoreAdditionalProjectSources>$(RestoreAdditionalProjectSources);$(MSBuildThisFileDirectory)core/artifacts</RestoreAdditionalProjectSources>
</PropertyGroup>
</Project>
28 changes: 28 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project>
<!-- Expands every <NordsteinCoreReference Include="Nordstein.Core.X" /> into the reference
kind the current mode calls for. See the comment in Directory.Build.props for why the
indirection exists.

This lives in Directory.Build.targets rather than .props because the item is declared in
the project body, which MSBuild evaluates after .props and before .targets.

Core's own projects reference each other with plain ProjectReferences and never declare
the item, so this is a no-op inside core/. -->

<ItemGroup Condition="'$(UseLocalCore)' == 'true'">
<ProjectReference Include="@(NordsteinCoreReference->'$(NordsteinCorePath)%(Identity)/%(Identity).csproj')" />
</ItemGroup>

<ItemGroup Condition="'$(UseLocalCore)' != 'true'">
<PackageReference Include="@(NordsteinCoreReference)" Version="$(NordsteinCoreVersion)" />
</ItemGroup>

<!-- Package mode against a version that was never restored fails deep inside NuGet with a
message that does not mention Core at all. Fail here instead, where the fix is obvious. -->
<Target Name="ValidateNordsteinCoreMode"
BeforeTargets="CollectPackageReferences"
Condition="'$(UseLocalCore)' != 'true' AND '@(NordsteinCoreReference)' != ''">
<Error Condition="'$(NordsteinCoreVersion)' == ''"
Text="NordsteinCoreVersion must be set when building in package mode (UseLocalCore=false)." />
</Target>
</Project>
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/AgentCallsControllerHistogramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
using Proxytrace.Domain.AgentCall;
using Proxytrace.Domain.Completion;
using Proxytrace.Domain.Session;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/AgentCallsControllerProposalsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
using Proxytrace.Domain.AuditLog;
using Proxytrace.Domain.TestSuite;
using Proxytrace.Licensing;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/AgentCallsControllerSummaryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
using Proxytrace.Domain.AuditLog;
using Proxytrace.Domain.Completion;
using Proxytrace.Domain.Session;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/AgentCallsControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
using Proxytrace.Domain.Completion;
using Proxytrace.Domain.Message;
using Proxytrace.Domain.Usage;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/AgentsControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
using Proxytrace.Domain.AgentCall;
using Proxytrace.Domain.ModelEndpoint;
using Proxytrace.Domain.Project;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/AnomaliesControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
using Proxytrace.Domain.CustomAnomaly;
using Proxytrace.Domain.Message;
using Proxytrace.Domain.Usage;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/AuditLogControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
using Proxytrace.Domain.AuditLog;
using Proxytrace.Domain.Project;
using Proxytrace.Domain.User;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
4 changes: 2 additions & 2 deletions Proxytrace.Api.Tests/Auth/ApiKeyAuthenticationHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
using Proxytrace.Api.Auth;
using Proxytrace.Api.Auth.Mcp;
using Proxytrace.Api.Auth.Rest;
using Proxytrace.Common.Security;
using Nordstein.Core.Common.Security;
using Proxytrace.Domain;
using Proxytrace.Domain.ApiKey;
using Proxytrace.Domain.ModelProvider;
using Proxytrace.Domain.Project;
using Proxytrace.Domain.User;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests.Auth;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/Auth/AuthUserResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using Proxytrace.Application.Auth;
using Proxytrace.Domain;
using Proxytrace.Domain.User;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests.Auth;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/Auth/SigningKeyProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Proxytrace.Api.Auth;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests.Auth;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/AuthControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
using Proxytrace.Domain.Notification;
using Proxytrace.Domain.User;
using Proxytrace.Licensing;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/Config/ConfigControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using NSubstitute;
using Proxytrace.Api.Controllers;
using Proxytrace.Domain.Kiosk;
using Proxytrace.Common.Hosting;
using Nordstein.Core.Common.Hosting;

namespace Proxytrace.Api.Tests.Config;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/CostLimitsControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
using Proxytrace.Domain.Project;
using Proxytrace.Domain.User;
using Proxytrace.Licensing;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
using Proxytrace.Domain.ModelEndpoint;
using Proxytrace.Domain.Project;
using Proxytrace.Licensing;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/EmailSettingsControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
using Proxytrace.Application.Notifications;
using Proxytrace.Domain.Notification;
using Proxytrace.Domain.User;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
2 changes: 1 addition & 1 deletion Proxytrace.Api.Tests/EvaluatorTestBenchControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
using Proxytrace.Domain.TestCase;
using Proxytrace.Domain.TestResult;
using Proxytrace.Domain.TestSuite;
using Proxytrace.Testing;
using Nordstein.Core.Testing;

namespace Proxytrace.Api.Tests;

Expand Down
Loading
Loading