Reviewed: 2026-10-02 · Code-grounded.
Testing & Validation is the map of what exists and what counts as validated. This page is the authoring guide: where a new test goes, which harness seam to use, and the traps that make a test flaky, slow, or silently vacuous. Read it before adding a suite.
| The thing you are testing | Project |
|---|---|
| An endpoint, hub, hosted service, auth policy, or anything needing the wired node host | XE-Local-AI-Engine.Tests |
| MAF/MEAI agent runtime — chat clients, tools, invocation, evals | XE-Local-AI-Engine.AI.Agent.Tests |
| A Graph Workflows parser, state-machine, document, dispatcher, executor or endpoint rule | XE-Local-AI-Engine.Tests (GraphWorkflows/, Endpoints/GraphWorkflows/V1/, Hubs/) — the module lives in Client.Application, not in the agent runtime |
| An EF migration, an entity store, the AEAD cipher, or the persistence contract | XE-Local-AI-Engine.Client.Persistence.Tests |
| A user-visible flow that must survive real routing, real SignalR, and a real browser | XE-Local-AI-Engine.Tests.E2ETests |
| A React component, hook, or mapper | colocated *.test.ts(x) next to the source in XE-Local-AI-Engine.Client.React/src/ |
| A release/packaging shell or Python script | scripts/tests/, scripts/compliance/tests/, scripts/performance/tests/, publish/tests/ |
Prefer the cheapest project that can still fail for the right reason. An endpoint's validation rules belong in
.Tests; the browser adds nothing and costs a frontend build.
Three rules hold for every test in this repo, in any language. The harness sections below exist to make them cheap to follow, not to stand in for them.
A test must not depend on another test having run, on execution order, or on state a previous run left behind.
- Name every row you write with a fresh
Guid/unique id, or take exclusive access (§2 shared-host rules, §3[NotInParallel]). - Restore every process-global mutation — env vars, static caches, ambient culture — in a
finally/Dispose, including on failure. - Never assert a whole-collection count or an empty state unless the class genuinely owns that state.
- Frontend:
restoreMocks,unstubEnvsandunstubGlobalsare on, andsrc/test/Cleanup.tsruns React Testing Library'scleanupafter each test — a Zustand store, alocalStoragekey or a module-levelletis still yours to reset inbeforeEach. - A loopback port obtained by binding
:0and releasing it is a candidate, not a reservation — another process on the host can take it before your child process or server binds it. Hold the listener when the port is the test's target, and go throughTesting/LoopbackPort.cs(Reserve+BindWithRetryAsync) when something else must bind it, retrying on the product's own in-use signal.
A test that passes only in isolation is not independent; it is broken and coincidentally green.
A test ends in an explicit assertion and is binary green/red with no human step in between.
- Assert with
AssertEx.*(C#),expect(…)(Vitest),assert/pytest.raises(Python),Should(Pester). A test that only exercises code has verified nothing.pnpm run validaterunsCheckTestsHaveAssertions.mjsover the frontend suite; no analyzer in this stack detects an assertion-less TUnit test at all, so on the backend the rule is enforced in review only. - Never read a log line, console output or a report to decide pass/fail. The assertion is the result.
- Never
returnearly because the OS, GPU or tool is missing — that reports a green pass. Skip visibly: TUnit's own[RunOn(OS.Linux)]/[ExcludeOn(OS.Windows)](using OS = TUnit.Core.Enums.OS;— that namespace'sLogLevelcollides with the logging one) for an OS gate,Skip.Test("<why>")after a probe for a capability you cannot name up front (Testing/SymlinkSupport.cs,Testing/JunctionSupport.cs). A guard that depends on more than the OS keepsSkip.Unless(...)in the body on top of the attribute.[RunOn]is invisible to the CA1416 platform analyzer, so a method that calls a Linux-only API also carries[UnsupportedOSPlatform("windows")]. Proof:XE-Local-AI-Engine.Tests/Testing/PlatformSkipTests.cs. - A test that logs a problem and stays green has the same defect as one with no assertion at all.
- A structural equality assertion —
AssertEx.Equal(expected, actual)on a whole object rather than on one of its members — is an assertion only while the type has value semantics. Convert that type from a record to a class and the same line becomes a reference comparison that the compiler accepts and the test keeps passing on nothing. Which shape a type is allowed to take, and the guards that pin it, are in Code Conventions.
Substitute only at a real collaboration boundary, and take the first of these that works:
- The real thing, when it is fast and deterministic — an in-memory store, a pure function, a real SQLite file.
- The repo's fake seam for that boundary:
FakeOllamafor anything model-dependent, MSW handlers for frontend network calls. Substitute.For<T>()— NSubstitute, never Moq or FakeItEasy, both banned — orvi.fn()/vi.mock()on the frontend, or stdlibunittest.mockin Python (notpytest-mock).- A hand-written fake, only when
Returns/Receivedgenuinely cannot express the behaviour. Reaching this rung is a signal to re-check rung 2.
NSubstitute.Analyzers.CSharp runs on all four test projects, so a substitute against a non-virtual member is a
build error instead of a silently passing test.
Never mock the thing the test exists to verify — a security or approval gate, the AEAD cipher, a migration's schema change. Substituting those proves only that the test calls the mock.
Every test class carries exactly one class-level [Category(...)], from three values:
| Category | What it means |
|---|---|
Unit |
Pure logic against in-memory collaborators: no host, no real database, no socket, no child process. A hand-written fake or an NSubstitute double is still Unit. |
Integration |
Deterministic, but boots something real in-process: a TestServerWebAppFactory host, a real SQLite file (including a MigratedDatabaseTemplate copy), a FakeOllama/FakeDocker server on a loopback socket, or a real child process. |
ExternalInfra |
Needs infrastructure the host may not have: a container daemon, a model runtime or live server, a GPU, a privileged sandbox binary. Opt-in through an environment variable, and every test in the class skips visibly when that gate is unset. Never part of the default gate. |
The line between Unit and Integration is mechanism, not speed: what the test starts decides its category,
not how long it takes. The gate runs Unit and Integration; the split exists so a failure's class is readable
from the lane that reported it.
Filter by category with TUnit's --treenode-filter property syntax — four path segments, then the property in
brackets (!= excludes; combine properties inside one bracket, [(Category=Unit)&(Other=Value)]):
--treenode-filter '/*/*/*/*[Category=Unit]'
--treenode-filter '/*/*/*/*[Category!=ExternalInfra]'Write the constant, never the string: [Category(TestCategories.Unit)]. Each test project declares its own
Categories/TestCategories.cs and imports it globally from the project file, so the attribute needs no using.
One trap the compiler will not tell you about: System.ComponentModel declares a CategoryAttribute too, and a
file-scoped using of that namespace silently wins over TUnit's — the class then carries no category at all while
the source still reads correctly. A file that imports that namespace therefore also aliases
using CategoryAttribute = TUnit.Core.CategoryAttribute;.
Architecture/TestCategoryConventionTests.cs is the guard. It fails on a class with [Test] methods that
carries no category or more than one (by reflection in its own assembly, by source scan in the sibling
projects), and on a Unit class that reaches an Integration mechanism — the host factory, a real SQLite
connection, a fake server, a real socket or a child process — directly or through any helper. Only the primitives
are written down; the helper list is derived at run time, so a new fixture is covered the day it lands, and there
is no allowlist. The written-down list includes the product composition root (Program.CreateAppAsync,
builder.AddServices, AddNodeApplication, AddNodeModelRuntime) — AddNodeModelRuntime is the only place in
the whole DI path that calls UseSqlite, so a test that touches any of those four reaches a real SQLite database
through product code that no scan over test sources could follow.
What the guard deliberately does not check is that an ExternalInfra class is tagged as one. "Every test in
this class is gated" is not decidable from a scan: the gate is usually a private helper several call levels below
the test, and some classes are mixed — WhisperRuntimeLiveSmokeTests and LlamaServerAdapterIntegrationTests
each pair gated live tests with tests that run unconditionally, so they are Integration, not ExternalInfra.
Tagging a mixed class ExternalInfra would drop its ungated tests out of the default gate. That call stays with
the reviewer — but the half that IS decidable is enforced: a *RealDaemonTests class must be ExternalInfra,
and a *LiveTests/*LiveSmokeTests class must not be Unit.
A test method name is subject + scenario + expected outcome, underscore-separated, matching the dominant shape
in every project: Subject_WhenScenario_ExpectedOutcome — Upload_WhenOversize_Rejects,
Import_WhenTheSelectedFolderInputIsRejected_ReturnsBadRequest. Drop the middle part only when the subject has a
single scenario worth naming (MigratedSchema_MatchesWhatEnsureCreatedBuilds). A name that states the mechanism
instead of the outcome ("…_Works", "…_Test") hides what broke when it reds.
XE-Local-AI-Engine.Tests/TestServerWebAppFactory.cs builds the real app through Program.CreateAppAsync and
serves it on TestServer. Never reintroduce WebApplicationFactory<Program> (docs/agent-knowledge.md §1).
Building a host is the single most expensive thing a backend test does. Share one per class when you can:
[ClassDataSource<TestServerWebAppFactory>(Shared = SharedType.PerClass)]
public required TestServerWebAppFactory Factory { get; init; }Then a test body starts var factory = Factory; instead of await using var factory = new TestServerWebAppFactory();.
See Integration/ApplicationStartupTests.cs and Endpoints/ModelFit/V1/ModelFitEndpointTests.cs.
Share only when all three hold:
- Every test either is read-only against host state, or names the rows it writes with a fresh
Guid/unique id. - No test asserts a whole-collection count or an empty state (
items.Length == 1,AssertEx.Empty(store)). Those assertions are what a sibling test's leftover row breaks. - Every test uses identical factory configuration.
A class may mix the two: give the shared host to the default-configuration tests and let the one test that
overrides configuration keep await using var factory = new TestServerWebAppFactory { … }. That is the shape of
Chat/NodeChatEndpointTests.cs and Endpoints/Agents/GetAgentPlaybookMonitorEndpointTests.cs.
Known blockers, so you do not rediscover them: skill names carry a unique index
(Configurations/AgentSkillConfiguration.cs), /api/local/v1/auth/setup bootstraps a single admin per host,
seeded agent templates are idempotent on a unique seed_slug, and resolving TracerProvider registers a
process-global ActivityListener (why ApiFoundation/BackendTraceCorrelationTests.cs stays per-test).
Agent-definition names are indexed but not unique, so duplicate seed names are safe.
A test that reads a container-singleton fake's history by POSITION — fake.Objectives[^1], fake.Created.Count,
fake.Calls.Single() — fails conditions 1 and 2 on a shared host, because that history is every sibling's too. The
fix is usually not a private host: read the same value off the row the test's own run owns, and the assertion both
survives sharing and names what it is about. DevWorkflowHarness.ReadObjectiveAsync(runId, nodeKey) is the worked
example — it answers the objective from the node run's own session row instead of the fake's list, which turned
seventeen positional reads in DevWorkflows/Execution/DevWorkflowAgentExecutorTests into id-scoped ones.
Never buy eligibility by resetting shared state. ClearReceivedCalls(), clearing a fake's list, or deleting rows
between tests makes the suite order-dependent instead of independent. If no id-scoped read expresses the claim — a
host-wide switch, a signal channel that drains, an absolute row count — that test keeps its own host and says so at
the construction site.
| Init property | Use it for |
|---|---|
ConfigureAdditionalTestServices |
Swap a service: services.RemoveAll<IFoo>(); services.AddSingleton(stub); |
AdditionalConfiguration |
Last-wins overlay of configuration keys |
EnableDevelopmentMode |
Turn Development Mode on/off for this host |
EnvironmentName |
Override the host environment — the way to exercise production-only middleware such as the rate limiter, which the Testing environment skips |
UsePreMigratedDatabase |
Default true: seed the host's SQLite file from the process-wide migrated template. Set false for a test that asserts on the migration path itself (empty database, pre-migration backup, pending-migration set); ignored when AdditionalConfiguration repoints ConnectionStrings:node-sqlite |
Auth helpers: CreateNodeAccessToken() / AddNodeBearerToken(request) mint an operator JWT;
CreateNonOperatorAccessToken() / AddNonOperatorBearerToken(request) mint an authenticated principal that
fails the operator policy — use that pair to prove a route is operator-gated, not merely authenticated.
If your test also persists the node-admin-test Identity row, seed
TestServerWebAppFactory.NodeAdminTestSecurityStamp verbatim or the fail-closed stamp check rejects the token.
TUnit runs classes — and tests within a class — in parallel.
- Bare
[NotInParallel]is a run-alone guard. It means "nothing else while this runs". Do not give it a key to "make it stricter"; a key does the opposite. - Keyed
[NotInParallel("X")]serializes on the shared resourceX. Every test that touchesXmust use the same key. Live examples:[NotInParallel("XE_NODE_SQLITE_KEY")],[NotInParallel("DevelopmentFeatureConfiguration")],[NotInParallel("XE_LLAMACPP_OVERRIDE_ENV")]. A test that touches several variables keys on all of them:Hosting/EngineCommandDispatchTests.cskeys onXE_DATA_DIR+XE_ADMIN_EMAIL+XE_ADMIN_PASSWORD.
Environment variables are process-global. A test that calls Environment.SetEnvironmentVariable must (a)
carry a keyed [NotInParallel("<VARIABLE_NAME>")] and (b) restore the previous value in a finally/Dispose,
including on failure. Leaking a variable poisons every later test in the module, in a way that reads as an
unrelated failure. See Providers/LlamaServer/OverrideSelectorAndOptionsTests.cs.
A sleep is either flaky (too short on a loaded box) or slow (too long everywhere). Use:
- a
TaskCompletionSourcethe code under test completes; Microsoft.Extensions.Time.Testing.FakeTimeProviderto advance time deterministically —Capabilities/CapabilityReporterTests.cs,Interaction/AskUserToolHandlerTests.cs;- an unbounded
Channelplus a bounded read, which turns "did the node do X?" into a wait with a real timeout and a legibleTimeoutException.
The same holds on the frontend: setTimeout, or await new Promise(r => setTimeout(r, n)), is the identical
defect. Use vi.useFakeTimers() or waitFor.
"X did not happen" needs the same discipline. Sleeping N ms and then asserting nothing happened can only fail
when the code gets slower — a real regression that fires the event late still reads green. Instead drive the code
to an observable blocking point through a gate the test controls, assert the negative there, then release the gate
and assert the positive. AssertEx.StaysIncompleteAsync(task, message) is the negative, called once the code has
observably reached its gate; AssertEx.CompletesAsync(task, TestBudgets.Contended, message) is the positive, with
a failure deadline. Both build on AssertEx.SettleAsync(), the primitive that drains the scheduler — it is
deterministic only when everything in flight is a thread-pool continuation, so a real timer or real I/O between the
call and the gate still needs a "reached" signal from a fake.
A real timer is allowed only when the subject is a real OS process, or when the delay is the subject's own input.
Mark it with a // real-timer: comment naming why, so a later reader does not "fix" it into a fake clock.
Unless RUN_LOCAL_INTEGRATION=true, the fixture starts a FakeOllamaServer seeded with
["qwen3.5:0.8b", "qwen3-embedding:0.6b"] and points the provider at it. Script it through the test-control
endpoints (POST /test/script, POST /test/failures, GET /test/requests) or by passing FakeOllamaOptions
to the factory constructor. Embeddings are SHA256-seeded and therefore stable
(Determinism/EmbeddingDeterminism.cs). Flip RUN_LOCAL_INTEGRATION=true only for a deliberate fidelity run
against a real local runtime — never as a CI default.
Point a real HubConnection at the fixture's in-memory transport — no sockets:
await using var connection = new HubConnectionBuilder()
.WithUrl("http://localhost" + LocalApiRoutes.LocalChat.Hub, options =>
{
options.HttpMessageHandlerFactory = _ => factory.Server.CreateHandler();
options.AccessTokenProvider = () => Task.FromResult<string?>(factory.CreateNodeAccessToken());
options.Headers.Add("Origin", "http://localhost");
})
.Build();The Origin header is not optional — the loopback Host/Origin guard rejects a hub negotiate without it, and a
foreign origin must be asserted to 403. Pattern: Chat/NodeChatHubTests.cs, Endpoints/Scheduler/SchedulerHubTests.cs.
Do not start the whole host and hope the loop ran. Resolve the service, drive one iteration through
Testing/BackgroundServiceTestHelper.RunExecuteAsync(service, ct), and cancel the token to end it. Give the class
a keyed [NotInParallel(nameof(YourBackgroundServiceTests))] when it touches a shared timer or connection.
Every migration that changes table/column/index shape ships a <MigrationName>MigrationTests.cs in
XE-Local-AI-Engine.Client.Persistence.Tests (the file follows the migration name, not an Add* prefix).
The shape is: start from the preceding migration id, insert historical rows, MigrateAsync() to head, then
assert the resulting schema — see AddAgentDefinitionsMigrationTests.cs. Use the shared schema probe
XE-Local-AI-Engine.Client.Persistence.Tests/Testing/MigrationSchemaProbe.cs for the table/column/index queries
rather than hand-rolling PRAGMA SQL per file. Remember MigrateAsync() applies every later migration too, so
assert the columns you added exist — not that the table's column set is exactly yours.
A database at head, or at migration N: copy the template. Replaying the declared chain against an empty file
is the dominant per-test cost in this project, and almost no test needs it. Testing/MigratedDatabaseTemplate.cs
builds each state once per process — keyed by the migrations assembly's module version id, so a rebuild
invalidates it — and every consumer gets a File.Copy. Reach for it as:
MigrationSchemaProbe.FromChatTemplateAsync(file) / FromChatTemplateAsync(file, predecessorId) /
FromIdentityTemplateAsync(file) for a probe, or MigratedDatabaseTemplate.CopyChatHeadAsync(path) /
CopyChatAtAsync(path, predecessorId) when the suite opens its own context. A WhenRolledBack_* test copies the
head template and then runs the down migration for real; a WhenApplied_* test copies the at-(N-1)
template and then runs the tail for real. Both still exercise the migration they are named after.
Foreign keys are ON, and a fixture must not quietly say otherwise. The node enforces them: the connection string
says Foreign Keys=True and NodeSqlitePragmas emits PRAGMA foreign_keys=ON on every open, so declared cascades
fire and Restrict relationships reject a parent deleted before its children. A fixture that appends
;Foreign Keys=False, or runs PRAGMA foreign_keys=OFF, is testing a database the node never has — the delete-order
bug it lets through is real, and the orphan state it lets a test seed may be unreachable in production. Pin the
posture only to seed a state the product genuinely produces, and say in a comment which one. Never infer the posture
from a connection string: a bare Data Source= sends no pragma at all, yet the bundled e_sqlite3 defaults it on.
Measure it with PRAGMA foreign_keys — NodeSqlitePragmasTests is where that is pinned.
A suite fixture that builds its schema with EnsureCreatedAsync() is the same mistake wearing a different hat — a
second definition of the schema, off the entity model. GraphWorkflowTestFixture copies the template instead;
the shared fixtures DevWorkflowTestFixture, ExternalAppTestFixture, IntegrationTestFixture,
WorkSessionPersistenceTestSupport and DevelopmentPersistenceTestSupport have not been moved, and dozens of store
suites still call EnsureCreatedAsync() inline — that is the remaining backlog. One test keeps EnsureCreated on
purpose: AddGraphWorkflowsMigrationTests.MigratedSchema_MatchesWhatEnsureCreatedBuilds holds its own context through
GraphWorkflowTestFixture.CreateEnsureCreatedSchemaAsync, because a parity test whose two sides both came from the
migrations asserts nothing. Either way the file is in WAL mode — EnsureCreated enables it exactly as the template
build does — and the at-rest scans stay honest because SqliteFileProbe.ReadAllBytesAsync closes the last
connection first, which checkpoints the log back into the main file.
That checkpoint only happens if the fixture opened the file with the exact bare Data Source=<path> string:
Microsoft.Data.Sqlite pools by the exact string, so a decorated one (;Cache=Shared, a query parameter) leaves its
own pool untouched and the at-rest scan reads stale bytes. SqliteFileProbeConnectionStringGuardTests
(Architecture/) scans every connection-string literal in the backend test projects, the shared context factories
included, and fails on a decorated one unless it is listed there with the reason it never reaches the probe.
Keep the from-empty replay (MigrationSchemaProbe.MigrateChatAsync / MigrateIdentityAsync) where the replay is
the thing under test, or where the assertion can see how the file was produced: a test that asserts the pending
migration set, that reads the migrator's own pre-migration backup file, that asserts a file was created, or that
asserts a journal mode or a PRAGMA. MigrationChainTests is the standing example — it asserts that the applied
set equals the declared set, which a template would make vacuous. Data seeded before migration N belongs on the
at-(N-1) template plus the real tail, never on a head copy.
A migration that converts, repairs or deletes data needs rows to convert, or it is only being tested as a
schema change. The probe's three-step seam is FromChatTemplateAsync(file, predecessorId) →
ExecuteAsync(insert…) → MigrateToAsync(thisMigrationId): copy the at-(N-1) template, seed the historical
rows through raw SQL (the entity model describes head, not the schema those rows were valid under), then run
exactly the one migration over them for real and assert what it did. EncryptConversationTitleMigrationTests.cs
(titles cleared) and RepairAndUniqueMessageSequenceMigrationTests.cs (colliding sequences renumbered) are the
worked examples.
Render through the shared provider wrapper src/test/RenderWithProviders.tsx (Mantine theme, TanStack
Query, router) instead of bare @testing-library/react — a bare render loses the providers most components
need. Translations need no wrapper: src/i18n.ts is a Vitest setupFiles entry, so every file resolves t()
against the shipped en bundle and must assert that string rather than the in-code defaultValue. Every
MantineProvider a test mounts — the shared wrapper and any hand-rolled one — carries env="test", Mantine's own
switch that disables transitions and renders portals inline, so no dropdown close timer or portal node outlives the
test that made it. Network goes through the MSW handlers in src/test/msw/; assert against handlers, not against a
mocked fetch. A file opts in with setupMswServer() (src/test/UseMswServer.ts), and inside such a file a request
no handler declared FAILS the test that made it, naming the method and URL: the rejection alone is not enough,
because a component reaching the API through TanStack Query catches it into query.error and a test asserting
elsewhere would stay green over a call it never stubbed. Declare the route, never widen the guard.
src/test/PinLocale.ts is already wired as a Vitest setupFiles entry, so locale is deterministic; so is
src/test/Cleanup.ts, which runs React Testing Library's cleanup after every test (Vitest does not register it
for you without globals); src/test/NoNetwork.ts is the global guard that fails any network call a test file did not stub. restoreMocks, unstubEnvs and unstubGlobals are on in vite.config.ts, so spies
and env stubs reset themselves — store and localStorage state does not. Every test needs a visible expect(…):
pnpm run validate runs CheckTestsHaveAssertions.mjs and fails on a test without one.
Gate it with TUnit's [RunOn(OS.Linux)] / [ExcludeOn(OS.Windows)] (OS is TUnit.Core.Enums.OS, imported as
using OS = TUnit.Core.Enums.OS; because that namespace's LogLevel collides with the logging one), never with
if (!OperatingSystem.IsWindows()) return; — an early return reports a green pass on every platform that cannot
run the test. Both are SkipAttribute subclasses whose reason names the platform; the proof test is
XE-Local-AI-Engine.Tests/Testing/PlatformSkipTests.cs. A guard that depends on more than the OS keeps
Skip.Unless(...) in the body on top of the attribute. [RunOn] is invisible to the CA1416 platform analyzer, so
a method that calls a Linux-only API also carries [UnsupportedOSPlatform("windows")]. When the gate is a
capability you have to probe rather than name (symlinks, NTFS junctions), probe once and call Skip.Test("<why>"),
the shape of Testing/SymlinkSupport.cs and Testing/JunctionSupport.cs.
Pick a base class, and pick it deliberately:
Common/XEPooledE2ETestBase.cs(groupBrowserPooled) — the default. The test leases one of the seeded pool users for its duration, so several browsers run at once. Requires that the test only reads node-global state or writesGuid-named rows.Common/XESerialE2ETestBase.cs(groupBrowserSerial) — for tests that mutate session-global state (theWorkerEventDispatcher.CurrentInvocationslot, FakeOllama scripts/models, the admin's tutorial row) or assert a node-wide empty state. Runs one at a time as the canonical admin.
The two groups run as disjoint phases, but which phase runs first is not guaranteed — never write a test that
depends on the other group having run. Selectors are testid-first (Page.GetByTestId("agent-create-button"));
note Mantine puts data-testid on the <input> for TextInput but on the wrapper for Textarea, so a
textarea needs [data-testid="…"] textarea. Traces are captured and written to test-results/traces/*.zip
only on failure — that zip is the first thing to open when CI reds.
# Backend — one class, or an alternation of several
scripts/with-build-lock.sh -- dotnet build XE-Local-AI-Engine.Tests/XE-Local-AI-Engine.Tests.csproj -c Release
scripts/with-build-lock.sh -- scripts/assembly-guard.sh guard --test-bins -- \
dotnet test XE-Local-AI-Engine.Tests/XE-Local-AI-Engine.Tests.csproj -c Release --no-build \
--treenode-filter '/*/*/(FooTests|BarTests)/*'
# Frontend
cd XE-Local-AI-Engine.Client.React && pnpm test -- src/features/agents
# E2E
scripts/run-e2e-local.sh --filter '/*/*/AgentsPageE2ETests/*' # --list enumerates without running--treenode-filter, never VSTest's--filter. Wildcards and(A|B)alternation both work. A filter that matches nothing exits 8; a zero-test run is not a pass.- Wrap every build and every test run in
scripts/with-build-lock.sh, and never build while a test run is in flight — the host is shared. Exit75means the result is void; rerun. - Iterate in Debug if you like, but finish with a Release build of the solution. Debug skips the analyzers entirely, so a green Debug build has verified none of the static-analysis wall.
- A bare
TODO/FIXMEin a C# comment fails the Release build (Sonar S1135 + warnings-as-errors). Describe the present limitation or rationale directly withoutTODO/FIXMEor task markers.
- Testing & Validation — topology, validation commands, CI gates, RC evidence
- Code Organization Conventions — where a file goes
- API & Hubs — the endpoint/hub surface under test
- Data & Persistence — entities and the migration timeline
- React Client — the frontend under test
- Home