From 0c6050e515d8d6fb314e9bee025a5639740305f5 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Sun, 2 Aug 2026 19:46:51 -0500 Subject: [PATCH] Adopt compliance wave 3: retire the last two Marten-ported event sourcing tests Closes #399. Companion to jasperfx#613 (epic marten#5110, marten#5118). Retired, and what replaced each: - Events/activity_correlation_tests.cs -> ActivityCorrelationCompliance - Projections/single_stream_projection_with_string_identity_tests.cs -> StringIdentitySingleStreamCompliance With those gone, no file in Polecat.Tests duplicates a Marten test except Linq/additional_linq_operator_tests.cs, which the shared library cannot absorb: LINQ is document-database behavior and JasperFx.Events.ComplianceTests spans only event sourcing. Polecat still re-declares IDocumentStore/IDocumentSession/ IQuerySession in its own namespace, so there is nothing for a document-db compliance suite to be generic over yet. Two markers, opposite meanings, now documented in CLAUDE.md and on the LINQ file. `ported:` means a file duplicates another store's test and should move into a shared suite. A bare `marten#NNNN` citation means Polecat implemented the same feature and the number is provenance worth keeping. Fourteen files carry the latter and none of them is porting work -- counting them is what made #399 originally read as sixteen files instead of three, and the count grows with every parity feature, so it can never converge. PolecatComplianceFixture picks up the seam the new suites need: StreamIdentity and EnableCorrelationTracking handling in BuildStoreAsync, plus CorrelationIdFor, CausationIdFor and SetCorrelationId. The alias file gains a fourth global using, and unlike the other three it names a closed generic, because the single stream projection base is generic over the identity type as well as the document. aggregateto_linq_operator_tests reached into the retired string-identity file for SelfAggregatingStringQuest. It now declares its own AggregateToStringQuest: the shared library's equivalent folds the compliance suite's events, not Polecat's. projection_sg_dispatch_audit_tests needed no change -- its rows bind to the compliance library's types by name, the way waves 1 and 2 already left it -- but docs/projection-sg-audit-108.md was citing four files that waves 1-3 deleted, so those rows now point at the suite that owns each type. Also adds a ComplianceSourceDir dev-loop escape hatch to Polecat.Tests.csproj: build with -p:ComplianceSourceDir= to compile a working copy of the suites instead of the package, which is how wave 3 was validated before the release. Compliance namespace 65/65, full suite 1660 with 0 failures. NOTE: needs JasperFx 2.37.3. Pins bumped ahead of the release; CI cannot restore until jasperfx#613 merges and ships. --- CLAUDE.md | 24 ++ Directory.Packages.props | 17 +- docs/projection-sg-audit-108.md | 42 +-- .../Compliance/ComplianceQuerySessionAlias.cs | 6 + .../Compliance/PolecatComplianceFixture.cs | 18 ++ .../polecat_event_store_compliance.cs | 6 + .../Events/activity_correlation_tests.cs | 116 -------- .../Linq/additional_linq_operator_tests.cs | 19 +- src/Polecat.Tests/Polecat.Tests.csproj | 9 +- .../aggregateto_linq_operator_tests.cs | 25 +- ...m_projection_with_string_identity_tests.cs | 248 ------------------ 11 files changed, 136 insertions(+), 394 deletions(-) delete mode 100644 src/Polecat.Tests/Events/activity_correlation_tests.cs delete mode 100644 src/Polecat.Tests/Projections/single_stream_projection_with_string_identity_tests.cs diff --git a/CLAUDE.md b/CLAUDE.md index 0391d650..9ed63603 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,6 +132,30 @@ Critical path for MVP: Stages 1–5, 7–8, 10–11 xunit.v3 is built against Microsoft.Testing.Platform 1.x; anything 2.x makes every test run die at startup with a `TypeLoadException`. See the comment in `Directory.Packages.props`. +### Cross-store compliance suites + +Event sourcing behavior that Polecat and Marten both have belongs in +`JasperFx.Events.ComplianceTests` (source-only package; the suites compile into `Polecat.Tests` so +JasperFx's aggregate source generator binds Polecat's session types). Enroll a suite with an empty +subclass in `Compliance/polecat_event_store_compliance.cs`, implement whatever seam it needs on +`PolecatComplianceFixture`, and delete the Polecat-local copy. Ten suites are enrolled across three +waves (#393, #400, #399). + +To iterate on a suite before the JasperFx release, build against a working copy instead of the +package: + +``` +dotnet build src/Polecat.Tests/Polecat.Tests.csproj \ + -p:ComplianceSourceDir=$HOME/code/jasperfx/src/JasperFx.Events.ComplianceTests +``` + +**Two markers, opposite meanings.** `ported:` on a test file means it *duplicates* a test that also +exists in another store and should eventually move into a shared suite — one file carries it today, +`Linq/additional_linq_operator_tests.cs`, blocked because there is no document-db compliance library. +A bare `marten#NNNN` citation means the opposite: Polecat implemented the same feature and the issue +number is provenance. Do not treat a `marten#NNNN` count as a porting backlog; it grows with every +parity feature and never converges. + ### Writing tests that survive being run in parallel processes The suite is a candidate for being run across several worker processes at once (Bobcat's supervisor diff --git a/Directory.Packages.props b/Directory.Packages.props index d4a4fe72..90e31b1a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -61,15 +61,20 @@ Also carries the jasperfx#594 patch half (a timed-out blue/green side-effect gate re-reads progression and succeeds when the replay had already reached the mark, plus a configurable DaemonSettings.SideEffectGateTimeout), jasperfx#595 (BatchingChannel could - deliver its trailing batch twice on shutdown) and #597. Same lockstep rule as above. --> - - + deliver its trailing batch twice on shutdown) and #597. Same lockstep rule as above. + JasperFx 2.37.3: compliance wave 3 (polecat#399) — JasperFx.Events.ComplianceTests gains + ActivityCorrelationCompliance and StringIdentitySingleStreamCompliance, plus the seam + those needed (ComplianceStoreConfig.StreamIdentity/.EnableCorrelationTracking and the + CorrelationIdFor/CausationIdFor/SetCorrelationId fixture members). Whole family moves in + lockstep even though only the compliance package changed. --> + + - + - + @@ -169,7 +174,7 @@ - + diff --git a/docs/projection-sg-audit-108.md b/docs/projection-sg-audit-108.md index 4481e98c..3080417c 100644 --- a/docs/projection-sg-audit-108.md +++ b/docs/projection-sg-audit-108.md @@ -17,6 +17,12 @@ ## How to read this report +- Types marked *(shared: SomethingCompliance)* are declared in + `JasperFx.Events.ComplianceTests` rather than in a Polecat test file. That + package is source-only, so they compile inside `Polecat.Tests` and the source + generator treats them exactly like local types. They moved there in compliance + waves 1–3 (#393, #400, #399); everything else cites the file it lives in. + - **✅ Generated**: SG emits a `[GeneratedEvolver]` partial / standalone evolver / `EventProjection` partial override; runtime registration finds the dispatcher; the post-#276 fail-fast does not fire. @@ -59,16 +65,16 @@ methods and emits a standalone `TEvolver` class. | 9 | `Report` *(Events/project_latest_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._Report_System_GuidEvolver.g.cs` | ✅ | | 10 | `StringReport` *(Events/project_latest_tests.cs)* | `string` | Apply + Create | ✓ | `..._StringReport_stringEvolver.g.cs` | ✅ | | 11 | `ScenarioQuestParty` *(Events/projection_scenario_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._ScenarioQuestParty_System_GuidEvolver.g.cs` | ✅ | -| 12 | `MutableIEventEvolveAggregate` *(Events/self_aggregating_evolve_method.cs)* | `Guid` | `Evolve(snapshot, IEvent)` | ✗ | `..._MutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | -| 13 | `MutableObjectEvolveAggregate` *(Events/self_aggregating_evolve_method.cs)* | `Guid` | `Evolve(snapshot, TEvent)` | ✗ | `..._MutableObjectEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | -| 14 | `ImmutableIEventEvolveAggregate` *(record; Events/self_aggregating_evolve_method.cs)* | `Guid` | `Evolve(snapshot, IEvent)` returning new record | ✗ | `..._ImmutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | +| 12 | `MutableIEventEvolveAggregate` *(shared: SelfAggregatingEvolveCompliance)* | `Guid` | `Evolve(snapshot, IEvent)` | ✗ | `..._MutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | +| 13 | `MutableObjectEvolveAggregate` *(shared: SelfAggregatingEvolveCompliance)* | `Guid` | `Evolve(snapshot, TEvent)` | ✗ | `..._MutableObjectEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | +| 14 | `ImmutableIEventEvolveAggregate` *(record; shared: SelfAggregatingEvolveCompliance)* | `Guid` | `Evolve(snapshot, IEvent)` returning new record | ✗ | `..._ImmutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | | 15 | `ImmutableObjectEvolveAggregate` *(record; same file)* | `Guid` | `Evolve(snapshot, TEvent)` returning new record | ✗ | `..._ImmutableObjectEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | -| 16 | `AsyncEvolveAggregate` *(Events/self_aggregating_evolve_method.cs)* | `Guid` | `EvolveAsync` | ✗ | `..._AsyncEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | +| 16 | `AsyncEvolveAggregate` *(shared: SelfAggregatingEvolveCompliance)* | `Guid` | `EvolveAsync` | ✗ | `..._AsyncEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | | 17 | `ImmutableAsyncEvolveAggregate` *(record; same file)* | `Guid` | `EvolveAsync` returning new record | ✗ | `..._ImmutableAsyncEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | | 18 | `CompositeQuestParty` *(Projections/composite_projection_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._CompositeQuestParty_System_GuidEvolver.g.cs` | ✅ | | 19 | `QuestStats` *(Projections/composite_projection_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._QuestStats_System_GuidEvolver.g.cs` | ✅ | | 20 | `QuestParty` *(Projections/inline_projection_tests.cs)* | `Guid` | Apply + Create + ShouldDelete | ✓ | `..._QuestParty_System_GuidEvolver.g.cs` | ✅ | -| 21 | `SelfAggregatingStringQuest` *(Projections/single_stream_projection_with_string_identity_tests.cs)* | `string` | Apply + Create | ✓ | `..._SelfAggregatingStringQuest_stringEvolver.g.cs` | ✅ | +| 21 | `SelfAggregatingStringQuest` *(shared: StringIdentitySingleStreamCompliance)* | `string` | Apply + Create | ✓ | `..._SelfAggregatingStringQuest_stringEvolver.g.cs` | ✅ | | 22 | `SnapshotParty` *(Projections/snapshot_registration_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._SnapshotParty_System_GuidEvolver.g.cs` | ✅ | | 23 | `SnapshotPartyByString` *(Projections/snapshot_registration_tests.cs)* | `string` | Apply + Create | ✓ | `..._SnapshotPartyByString_stringEvolver.g.cs` | ✅ | | 24 | `Payment` *(Projections/using_guid_based_strong_typed_id_for_aggregate_identity.cs)* | `PaymentId` *(wraps Guid)* | Apply + Create | ✓ | `..._Payment_Polecat_Tests_Projections_PaymentIdEvolver.g.cs` | ✅ | @@ -90,13 +96,13 @@ override) — **`partial` required on the class** for these shapes. |---|---|---|---|---|---|---| | 1 | `QuestLogProjection` *(Projections/event_projection_tests.cs)* | `EventProjection` | conventional `Project(TEvent, IDocumentSession)` | ✓ | `..._QuestLogProjection.EventProjection.g.cs` | ✅ | | 2 | `MultiEventQuestLogProjection` *(Projections/event_projection_tests.cs)* | `EventProjection` | conventional `Project(TEvent, IDocumentSession)` × 2 | ✓ | `..._MultiEventQuestLogProjection.EventProjection.g.cs` | ✅ | -| 3 | `SimpleEnrichmentProjection` *(Projections/event_projection_enrichment_tests.cs)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._SimpleEnrichmentProjection.EventProjection.g.cs` | ✅ (EnrichEventsAsync is a side-channel; dispatch still through SG-emitted `Project`) | -| 4 | `EnrichmentCallOrderProjection` *(Projections/event_projection_enrichment_tests.cs)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._EnrichmentCallOrderProjection.EventProjection.g.cs` | ✅ | -| 5 | `DbLookupEnrichmentProjection` *(Projections/event_projection_enrichment_tests.cs)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._DbLookupEnrichmentProjection.EventProjection.g.cs` | ✅ | -| 6 | `AuditRecordProjection` *(Projections/event_projection_should_register_document_types.cs)* | `EventProjection` | overrides `ApplyAsync` directly | ✓ | `..._AuditRecordProjection.TypeRegistration.g.cs` *(TypeRegistration-only)* | ✅ — dispatch via `ApplyAsync` override; SG emits only the `RegisterPublishedType` constructor so `Schema.For` is discoverable (per [marten#4166](https://github.com/JasperFx/marten/issues/4166)) | -| 7 | `AuditRecordCreatorProjection` *(Projections/event_projection_should_register_document_types.cs)* | `EventProjection` | conventional `Create(AuditableEvent)` returning the doc type | ✓ | `..._AuditRecordCreatorProjection.EventProjection.g.cs` | ✅ | +| 3 | `SimpleEnrichmentProjection` *(shared: EventProjectionEnrichmentCompliance)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._SimpleEnrichmentProjection.EventProjection.g.cs` | ✅ (EnrichEventsAsync is a side-channel; dispatch still through SG-emitted `Project`) | +| 4 | `EnrichmentCallOrderProjection` *(shared: EventProjectionEnrichmentCompliance)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._EnrichmentCallOrderProjection.EventProjection.g.cs` | ✅ | +| 5 | `DbLookupEnrichmentProjection` *(shared: EventProjectionEnrichmentCompliance)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._DbLookupEnrichmentProjection.EventProjection.g.cs` | ✅ | +| 6 | `AuditRecordProjection` *(shared: EventProjectionRegistrationCompliance)* | `EventProjection` | overrides `ApplyAsync` directly | ✓ | `..._AuditRecordProjection.TypeRegistration.g.cs` *(TypeRegistration-only)* | ✅ — dispatch via `ApplyAsync` override; SG emits only the `RegisterPublishedType` constructor so `Schema.For` is discoverable (per [marten#4166](https://github.com/JasperFx/marten/issues/4166)) | +| 7 | `AuditRecordCreatorProjection` *(shared: EventProjectionRegistrationCompliance)* | `EventProjection` | conventional `Create(AuditableEvent)` returning the doc type | ✓ | `..._AuditRecordCreatorProjection.EventProjection.g.cs` | ✅ | | 8 | `ImportSqlProjection` *(Projections/using_event_projection_for_flat_tables.cs)* | `EventProjection` | conventional `Project(TEvent, IDocumentSession)` × 2 | ✓ | `..._ImportSqlProjection.EventProjection.g.cs` | ✅ | -| 9 | `StringQuestPartyProjection` *(Projections/single_stream_projection_with_string_identity_tests.cs)* | `SingleStreamProjection` | conventional Apply/Create + ShouldDelete on the **projection** (delegating to doc would be the alternative) | ✓ | `..._StringQuestPartyProjection.Evolver.g.cs` *(PartialProjection)* | ✅ | +| 9 | `StringQuestPartyProjection` *(shared: StringIdentitySingleStreamCompliance)* | `SingleStreamProjection` | conventional Apply/Create + ShouldDelete on the **projection** (delegating to doc would be the alternative) | ✓ | `..._StringQuestPartyProjection.Evolver.g.cs` *(PartialProjection)* | ✅ | | 10 | `CustomerSummaryProjection` *(Projections/multi_stream_projection_tests.cs)* | `MultiStreamProjection` | conventional Apply on the projection | ✓ | `..._CustomerSummaryProjection.Evolver.g.cs` | ✅ | | 11 | `MonthlyAccountActivityProjection` *(Projections/time_based_multi_stream_projection_tests.cs)* | `MultiStreamProjection` | conventional Apply on the projection | ✓ | `..._MonthlyAccountActivityProjection.Evolver.g.cs` | ✅ | | 12 | `CompositeOrderProjection` *(Projections/composite_try_find_upstream_cache_tests.cs)* | `SingleStreamProjection` | **overrides `Evolve` directly** | ✓ | — | ⚠ Deliberate bypass — override wins per #276 doctrine. Composite-projection test exercises upstream-cache lookup, not dispatch correctness. | @@ -151,15 +157,15 @@ through the projection and asserts state evolves correctly: |---|---| | Self-aggregating sync Apply + Create | `fetch_for_writing_tests`, `Bug_4197_fetch_for_writing_natural_key` | | Self-aggregating Apply + Create + **ShouldDelete** | `aggregate_stream_to_last_known_tests` (DeletableAggregate), `inline_projection_tests` (QuestParty) | -| Self-aggregating sync `Evolve(snapshot, IEvent)` (mutable + immutable record) | `self_aggregating_evolve_method` (×4 variants) | -| Self-aggregating `EvolveAsync` | `self_aggregating_evolve_method` (×2 variants) | -| String identity | `single_stream_projection_with_string_identity_tests`, `project_latest_tests` (StringReport), `always_enforce_consistency_with_string_stream_id` | +| Self-aggregating sync `Evolve(snapshot, IEvent)` (mutable + immutable record) | `SelfAggregatingEvolveCompliance` (×4 variants) | +| Self-aggregating `EvolveAsync` | `SelfAggregatingEvolveCompliance` (×2 variants) | +| String identity | `StringIdentitySingleStreamCompliance`, `project_latest_tests` (StringReport), `always_enforce_consistency_with_string_stream_id` | | Strong-typed-id identity (wrapper struct) | `using_guid_based_strong_typed_id_for_aggregate_identity`, `using_string_based_strong_typed_id_for_aggregate_identity` | | `EventProjection` with conventional `Project(TEvent, IDocumentSession)` | `event_projection_tests` | -| `EventProjection` with `Create(TEvent)` returning a doc | `event_projection_should_register_document_types` (AuditRecordCreatorProjection) | -| `EventProjection` with `ApplyAsync` override | `event_projection_should_register_document_types` (AuditRecordProjection) | -| `EventProjection` with `EnrichEventsAsync` override | `event_projection_enrichment_tests` (×3 variants) | -| `SingleStreamProjection` subclass with Apply on the projection | `single_stream_projection_with_string_identity_tests` (StringQuestPartyProjection) | +| `EventProjection` with `Create(TEvent)` returning a doc | `EventProjectionRegistrationCompliance` (AuditRecordCreatorProjection) | +| `EventProjection` with `ApplyAsync` override | `EventProjectionRegistrationCompliance` (AuditRecordProjection) | +| `EventProjection` with `EnrichEventsAsync` override | `EventProjectionEnrichmentCompliance` (×3 variants) | +| `SingleStreamProjection` subclass with Apply on the projection | `StringIdentitySingleStreamCompliance` (StringQuestPartyProjection) | | `MultiStreamProjection` subclass with Apply on the projection | `multi_stream_projection_tests`, `time_based_multi_stream_projection_tests` | | `PolecatCompositeProjection` with `Snapshot` | `composite_projection_tests` | | `PolecatCompositeProjection` with `TryFindUpstreamCache` between stages | `composite_try_find_upstream_cache_tests` | diff --git a/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs b/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs index f730b274..78d5853e 100644 --- a/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs +++ b/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs @@ -8,3 +8,9 @@ // they cannot reach the pair their suite class is generic over. global using ComplianceOperations = Polecat.IDocumentSession; global using ComplianceEventProjection = Polecat.Projections.EventProjection; + +// The string-identity suite's custom projection needs Polecat's own SingleStreamProjection base, and +// that one is generic over the identity type as well as the document, so this alias names a closed +// generic rather than an open one. +global using ComplianceStringPartyProjectionBase = + Polecat.Projections.SingleStreamProjection; diff --git a/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs b/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs index 3a073925..388b0169 100644 --- a/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs +++ b/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs @@ -37,6 +37,17 @@ protected override async Task BuildStoreAsync(ComplianceStoreConfig config) options.DaemonSettings.MaxConcurrentRebuildsPerDatabase = config.MaxConcurrentRebuildsPerDatabase; } + if (config.StreamIdentity.HasValue) + { + options.Events.StreamIdentity = config.StreamIdentity.Value; + } + + if (config.EnableCorrelationTracking) + { + options.Events.EnableCorrelationId = true; + options.Events.EnableCausationId = true; + } + config.ApplyTo(new PolecatComplianceRegistrar(options)); _store = new DocumentStore(options); @@ -81,6 +92,13 @@ public override Task SaveChangesAsync(IDocumentSession session, CancellationToke public override IEventStoreOperations EventsFor(IDocumentSession session) => session.Events; + public override string? CorrelationIdFor(IDocumentSession session) => session.CorrelationId; + + public override string? CausationIdFor(IDocumentSession session) => session.CausationId; + + public override void SetCorrelationId(IDocumentSession session, string? correlationId) + => session.CorrelationId = correlationId; + public override IEventStore EventStore => _store; public override IEnumerable AllAggregateTypes() => _store.Options.Projections.AllAggregateTypes(); diff --git a/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs b/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs index f4d505d9..e3693f75 100644 --- a/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs +++ b/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs @@ -33,3 +33,9 @@ public class event_projection_enrichment_compliance public class rebuild_concurrency_cap_compliance : RebuildConcurrencyCapCompliance; + +public class activity_correlation_compliance + : ActivityCorrelationCompliance; + +public class string_identity_single_stream_compliance + : StringIdentitySingleStreamCompliance; diff --git a/src/Polecat.Tests/Events/activity_correlation_tests.cs b/src/Polecat.Tests/Events/activity_correlation_tests.cs deleted file mode 100644 index 7b401283..00000000 --- a/src/Polecat.Tests/Events/activity_correlation_tests.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Diagnostics; -using Polecat.Tests.Harness; - -namespace Polecat.Tests.Events; - -/// -/// #239: a session auto-seeds CorrelationId from Activity.Current.RootId and CausationId from -/// Activity.Current.ParentId on open, so distributed-tracing context flows onto events with zero -/// app code (mirrors Marten). An explicit caller value still wins. -/// -public class activity_correlation_tests : OneOffConfigurationsContext -{ - private async Task ConfigureAndApply(Action configure) - { - ConfigureStore(configure); - await theDatabase.ApplyAllConfiguredChangesToDatabaseAsync(); - } - - private static (Activity parent, Activity child) StartActivityScope() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - var parent = new Activity("parent").Start(); - var child = new Activity("child").Start(); // child of parent → has a non-null ParentId - return (parent, child); - } - - [Fact] - public async Task session_seeds_correlation_and_causation_from_activity() - { - await ConfigureAndApply(_ => { }); - - var (parent, child) = StartActivityScope(); - try - { - await using var session = theStore.LightweightSession(); - session.CorrelationId.ShouldBe(child.RootId); - session.CausationId.ShouldBe(child.ParentId); - } - finally - { - child.Stop(); - parent.Stop(); - } - } - - [Fact] - public async Task events_appended_in_activity_scope_carry_root_and_parent_ids() - { - await ConfigureAndApply(opts => - { - opts.Events.EnableCorrelationId = true; - opts.Events.EnableCausationId = true; - }); - - var (parent, child) = StartActivityScope(); - var streamId = Guid.NewGuid(); - try - { - await using var session = theStore.LightweightSession(); - session.Events.StartStream(streamId, new QuestStarted("Traced Quest")); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - } - finally - { - child.Stop(); - parent.Stop(); - } - - await using var query = theStore.QuerySession(); - var events = await query.Events.FetchStreamAsync(streamId, token: TestContext.Current.CancellationToken); - - events.Count.ShouldBe(1); - events[0].CorrelationId.ShouldBe(child.RootId); - events[0].CausationId.ShouldBe(child.ParentId); - } - - [Fact] - public async Task explicit_caller_value_wins_over_activity() - { - await ConfigureAndApply(_ => { }); - - var (parent, child) = StartActivityScope(); - try - { - await using var session = theStore.LightweightSession(); - session.CorrelationId = "explicit-corr"; - session.CorrelationId.ShouldBe("explicit-corr"); - session.CorrelationId.ShouldNotBe(child.RootId); - } - finally - { - child.Stop(); - parent.Stop(); - } - } - - [Fact] - public async Task no_activity_leaves_correlation_null() - { - await ConfigureAndApply(_ => { }); - - // Ensure no ambient activity for this test. - var saved = Activity.Current; - Activity.Current = null; - try - { - await using var session = theStore.LightweightSession(); - session.CorrelationId.ShouldBeNull(); - session.CausationId.ShouldBeNull(); - } - finally - { - Activity.Current = saved; - } - } -} diff --git a/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs b/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs index 0d6894f8..a62f4c6a 100644 --- a/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs +++ b/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs @@ -4,9 +4,24 @@ namespace Polecat.Tests.Linq; /// -/// Tests for LINQ operators ported from Marten: negation, modulo, Last/LastOrDefault, -/// .Equals(), nullable types, and HasValue. +/// ported: Marten — LINQ operators duplicated from Marten's suite: negation, modulo, +/// Last/LastOrDefault, .Equals(), nullable types, and HasValue. /// +/// +/// The last remaining duplication of a Marten test after compliance wave 3 (#399), and the one +/// the shared library cannot absorb: LINQ is document-database behavior, and +/// JasperFx.Events.ComplianceTests spans only the event sourcing surface. Polecat still +/// re-declares IDocumentStore/IDocumentSession/IQuerySession in its own +/// namespace, so there is nothing for a document-db compliance suite to be generic over yet; +/// Weasel.Storage is the intended convergence layer. Tracked in marten#5118. +/// +/// +/// Marker convention: ported: means this file duplicates a test that also exists in +/// another store and should eventually move into a shared suite. A bare marten#NNNN +/// citation elsewhere in this suite means the opposite — Polecat implemented the same +/// feature and the issue number is provenance worth keeping. +/// +/// public class additional_linq_operator_tests : OneOffConfigurationsContext { private async Task StoreSeedDataAsync() diff --git a/src/Polecat.Tests/Polecat.Tests.csproj b/src/Polecat.Tests/Polecat.Tests.csproj index 1714f2fe..5cecd77c 100644 --- a/src/Polecat.Tests/Polecat.Tests.csproj +++ b/src/Polecat.Tests/Polecat.Tests.csproj @@ -30,7 +30,14 @@ - + + + diff --git a/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs b/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs index 6a71f302..32a0b743 100644 --- a/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs +++ b/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs @@ -8,8 +8,27 @@ namespace Polecat.Tests.Projections; // #364 follow-up (Marten parity): AggregateToAsync() folds every event matched by an event query // into a single aggregate, regardless of stream, optionally starting from supplied state. The // aggregate's identity is stamped from the last queried event's stream. Reuses the self-aggregating -// QuestParty (inline_projection_tests.cs) and SelfAggregatingStringQuest -// (single_stream_projection_with_string_identity_tests.cs) declared in this namespace. +// QuestParty (inline_projection_tests.cs) declared in this namespace. + +/// +/// String-keyed self-aggregating type for the key-identity case below. +/// +/// +/// Lived in single_stream_projection_with_string_identity_tests.cs until that file was retired +/// into JasperFx.Events.ComplianceTests (#399). The shared library's equivalent folds the +/// compliance suite's own events, so this test keeps a local type that folds Polecat's. +/// +public partial class AggregateToStringQuest +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public List Members { get; set; } = new(); + + public static AggregateToStringQuest Create(QuestStarted e) => new() { Name = e.Name }; + + public void Apply(MembersJoined e) => Members.AddRange(e.Members); +} + public class aggregateto_linq_operator_tests : OneOffConfigurationsContext { private readonly MembersJoined _joined1 = new(1, "Emond's Field", ["Rand", "Matrim", "Perrin", "Thom"]); @@ -115,7 +134,7 @@ public async Task gets_the_key_set() var quest = await session.Events.QueryAllRawEvents() .Where(x => x.StreamKey == key) - .AggregateToAsync(token: TestContext.Current.CancellationToken); + .AggregateToAsync(token: TestContext.Current.CancellationToken); quest.ShouldNotBeNull(); quest.Id.ShouldBe(key); diff --git a/src/Polecat.Tests/Projections/single_stream_projection_with_string_identity_tests.cs b/src/Polecat.Tests/Projections/single_stream_projection_with_string_identity_tests.cs deleted file mode 100644 index 0aa10bfb..00000000 --- a/src/Polecat.Tests/Projections/single_stream_projection_with_string_identity_tests.cs +++ /dev/null @@ -1,248 +0,0 @@ -using JasperFx.Events; -using JasperFx.Events.Projections; -using Polecat.Projections; -using Polecat.Tests.Harness; - -namespace Polecat.Tests.Projections; - -/// -/// Aggregate document with a string identity, for use with string-keyed streams. -/// -public partial class StringQuestParty -{ - public string Id { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public List Members { get; set; } = new(); - public string? Location { get; set; } - public List MonstersSlain { get; set; } = new(); -} - -/// -/// Self-aggregating document type with string identity, for use with -/// the Snapshot<T, TId> API (conventional Apply/Create methods on the type itself). -/// -public partial class SelfAggregatingStringQuest -{ - public string Id { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public List Members { get; set; } = new(); - - public static SelfAggregatingStringQuest Create(QuestStarted e) - { - return new SelfAggregatingStringQuest { Name = e.Name }; - } - - public void Apply(MembersJoined e) - { - Members.AddRange(e.Members); - } -} - -/// -/// Custom SingleStreamProjection with TId = string, demonstrating the new -/// two-type-parameter signature that mirrors Marten's SingleStreamProjection<TDoc, TId>. -/// -public partial class StringQuestPartyProjection : SingleStreamProjection -{ - public StringQuestPartyProjection() - { - } - - public static StringQuestParty Create(IEvent @event) - { - return new StringQuestParty - { - Id = @event.StreamKey!, - Name = @event.Data.Name - }; - } - - public void Apply(MembersJoined e, StringQuestParty party) - { - party.Members.AddRange(e.Members); - party.Location = e.Location; - } - - public void Apply(MembersDeparted e, StringQuestParty party) - { - foreach (var m in e.Members) party.Members.Remove(m); - } - - public void Apply(ArrivedAtLocation e, StringQuestParty party) - { - party.Location = e.Location; - } - - public void Apply(MonsterSlain e, StringQuestParty party) - { - party.MonstersSlain.Add(e.Name); - } - - public bool ShouldDelete(QuestEnded e) - { - return true; - } -} - -[Collection("integration")] -public class single_stream_projection_with_string_identity_tests : IntegrationContext -{ - public single_stream_projection_with_string_identity_tests(DefaultStoreFixture fixture) : base(fixture) - { - } - - private async Task CreateStoreWithStringProjection() - { - await StoreOptions(opts => - { - opts.DatabaseSchemaName = "string_proj"; - opts.Events.StreamIdentity = StreamIdentity.AsString; - opts.Projections.Add(JasperFx.Events.Projections.ProjectionLifecycle.Inline); - }); - return theStore; - } - - [Fact] - public async Task custom_projection_creates_aggregate_on_start_stream() - { - var store = await CreateStoreWithStringProjection(); - - var streamKey = "quest-" + Guid.NewGuid(); - await using var session = store.LightweightSession(); - session.Events.StartStream(streamKey, - new QuestStarted("Destroy the Ring")); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = store.QuerySession(); - var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken); - - party.ShouldNotBeNull(); - party.Id.ShouldBe(streamKey); - party.Name.ShouldBe("Destroy the Ring"); - } - - [Fact] - public async Task custom_projection_applies_multiple_events() - { - var store = await CreateStoreWithStringProjection(); - - var streamKey = "quest-" + Guid.NewGuid(); - await using var session = store.LightweightSession(); - session.Events.StartStream(streamKey, - new QuestStarted("Fellowship"), - new MembersJoined(1, "Rivendell", ["Aragorn", "Legolas", "Gimli"])); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = store.QuerySession(); - var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken); - - party.ShouldNotBeNull(); - party.Name.ShouldBe("Fellowship"); - party.Members.ShouldBe(["Aragorn", "Legolas", "Gimli"]); - party.Location.ShouldBe("Rivendell"); - } - - [Fact] - public async Task custom_projection_updates_on_append() - { - var store = await CreateStoreWithStringProjection(); - - var streamKey = "quest-" + Guid.NewGuid(); - await using var session = store.LightweightSession(); - session.Events.StartStream(streamKey, - new QuestStarted("Adventure"), - new MembersJoined(1, "Start", ["Hero"])); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var session2 = store.LightweightSession(); - session2.Events.Append(streamKey, - new ArrivedAtLocation("Dungeon", 2), - new MonsterSlain("Goblin", 50)); - await session2.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = store.QuerySession(); - var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken); - - party.ShouldNotBeNull(); - party.Location.ShouldBe("Dungeon"); - party.MonstersSlain.ShouldContain("Goblin"); - } - - [Fact] - public async Task custom_projection_should_delete_removes_aggregate() - { - var store = await CreateStoreWithStringProjection(); - - var streamKey = "quest-" + Guid.NewGuid(); - await using var session = store.LightweightSession(); - session.Events.StartStream(streamKey, - new QuestStarted("Doomed Quest"), - new MembersJoined(1, "Start", ["Hero"])); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var session2 = store.LightweightSession(); - session2.Events.Append(streamKey, - new QuestEnded("Doomed Quest")); - await session2.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = store.QuerySession(); - var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken); - party.ShouldBeNull(); - } - - [Fact] - public async Task multiple_string_keyed_streams_projected_independently() - { - var store = await CreateStoreWithStringProjection(); - - var key1 = "quest-" + Guid.NewGuid(); - var key2 = "quest-" + Guid.NewGuid(); - - await using var session = store.LightweightSession(); - session.Events.StartStream(key1, - new QuestStarted("Quest 1"), - new MembersJoined(1, "Town A", ["Alpha"])); - session.Events.StartStream(key2, - new QuestStarted("Quest 2"), - new MembersJoined(1, "Town B", ["Beta", "Gamma"])); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = store.QuerySession(); - var party1 = await query.LoadAsync(key1, TestContext.Current.CancellationToken); - var party2 = await query.LoadAsync(key2, TestContext.Current.CancellationToken); - - party1.ShouldNotBeNull(); - party1.Name.ShouldBe("Quest 1"); - party1.Members.ShouldBe(["Alpha"]); - - party2.ShouldNotBeNull(); - party2.Name.ShouldBe("Quest 2"); - party2.Members.ShouldBe(["Beta", "Gamma"]); - } - - [Fact] - public async Task self_aggregating_projection_with_string_identity() - { - // Use SingleStreamProjection with a self-aggregating type - await StoreOptions(opts => - { - opts.DatabaseSchemaName = "string_snap"; - opts.Events.StreamIdentity = StreamIdentity.AsString; - opts.Projections.Add>(ProjectionLifecycle.Inline); - }); - - var streamKey = "snap-" + Guid.NewGuid(); - await using var session = theStore.LightweightSession(); - session.Events.StartStream(streamKey, - new QuestStarted("Snapshot Quest"), - new MembersJoined(1, "Castle", ["Knight"])); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = theStore.QuerySession(); - var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken); - - party.ShouldNotBeNull(); - party.Name.ShouldBe("Snapshot Quest"); - party.Members.ShouldBe(["Knight"]); - } -}