From 52b151abf742cf23a8f015f26f0399325186c68e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= <6334612+rbuergi@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:58:40 +0200 Subject: [PATCH 1/3] =?UTF-8?q?test(ladder):=20rows=20F1,=20F4=20and=20E3?= =?UTF-8?q?=20through=20the=20push=20path=20=E2=80=94=20no=20seal=20holds?= =?UTF-8?q?=20or=20redirects=20a=20push,=20a=20runtime=20node=20survives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ModuleUpdateLadderSyncTest drives the real webhook processor, sync service and importer (only the GitHub transport is substituted): - a push imports at the pushed commit with NO seal for the running identity and no green build (the precondition is read through SealedPublicationIndex, with a control seal under another identity); - a seal for this identity at an OLDER commit neither holds nor redirects the push; - a node created at runtime survives a push import while a retired source node is pruned. Negative controls run: re-inserting the seal hold reds both seal tests; restoring the FullReplace prune rule reds the runtime-node test at the second import, named. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ModuleUpdateLadderSyncTest.cs | 394 ++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs diff --git a/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs b/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs new file mode 100644 index 0000000000..d8ea47bfb4 --- /dev/null +++ b/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs @@ -0,0 +1,394 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using MeshWeaver.Fixture; +using MeshWeaver.GitSync; +using MeshWeaver.Graph; +using MeshWeaver.Graph.Configuration; +using MeshWeaver.Hosting.Monolith.TestBase; +using MeshWeaver.Markdown; +using MeshWeaver.Mesh; +using MeshWeaver.Mesh.Security; +using MeshWeaver.Mesh.Services; +using MeshWeaver.Reactive.Assertions; +using MeshWeaver.Utils; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace MeshWeaver.Hosting.Test; + +/// +/// 🚨 The source-sync rows of the module update ladder (Doc/Architecture/ModuleUpdateLadder, +/// rows E3, F1 and F4; policies sources-sync-on-push and prune-requires-provenance), +/// driven end to end through the real webhook processor, the real sync service and the real importer. +/// +/// Rows the sibling classes already pin are not repeated here: F2 ("a red build holds nothing") +/// is , +/// and F3 ("an incompatible module is declined alone") is +/// . +/// What this class adds is the SEAL half (E3): a published-bundle root that holds no seal for the +/// running identity, or one for an OLDER commit of the same repository, must neither hold nor +/// redirect a push. It also adds F4 through the push path, not only through the importer. +/// +/// One seam is substituted: the GitHub transport (), which is +/// the IO boundary and not a mesh interface. The published root is a real directory carrying the +/// markers the publishing lane writes. +/// +public class ModuleUpdateLadderSyncTest(ITestOutputHelper output) : MonolithMeshTestBase(output) +{ + private const string RepoFullName = "test/ladder-sync"; + private const string RepoUrl = $"https://github.com/{RepoFullName}"; + private const string SealedSourceName = "plugins"; + + /// An identity this process does NOT run — a newer line's seal. + private const string OtherIdentity = "c999e999"; + + private const string OlderSha = "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a"; + private const string PushedSha = "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b"; + private const string NextSha = "2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c"; + + private readonly TreeRepoClient repoClient = new(); + + private readonly string publishedRoot = Path.Combine( + Path.GetTempPath(), "mw-ladder-sync-" + Guid.NewGuid().ToString("N")[..12]); + + private static string UserId => TestUsers.Admin.ObjectId!; + + private GitHubSyncService Sync => Mesh.ServiceProvider.GetRequiredService(); + private GitHubCredentialService Credentials => Mesh.ServiceProvider.GetRequiredService(); + private GitHubWebhookProcessor Webhooks => Mesh.ServiceProvider.GetRequiredService(); + private IMeshService MeshService => Mesh.ServiceProvider.GetRequiredService(); + private AccessService Access => Mesh.ServiceProvider.GetRequiredService(); + + /// + protected override MeshBuilder ConfigureMesh(MeshBuilder builder) + { + Directory.CreateDirectory(publishedRoot); + return base.ConfigureMesh(builder) + .AddGitHubSyncTypes() + .ConfigureServices(services => + { + services.AddGitHubSyncServices(); + services.AddSingleton(repoClient); + // Layer the published-bundle root ONTO the host configuration (replacing it would take + // every other key with it — the measurement is in HeldSourceSaysItIsHeldTest). + var configured = services.LastOrDefault(d => d.ServiceType == typeof(IConfiguration)); + if (configured is not null) + services.Remove(configured); + return services.AddSingleton(sp => + { + var configuration = new ConfigurationBuilder(); + if (Materialise(configured, sp) is { } host) + configuration.AddConfiguration(host); + return configuration.AddInMemoryCollection(new Dictionary + { + [ShippedPrebuiltBundles.PublishedRootConfigKey] = publishedRoot, + }).Build(); + }); + }); + } + + /// + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync(); + try + { + if (Directory.Exists(publishedRoot)) + Directory.Delete(publishedRoot, recursive: true); + } + catch (IOException) + { + // A temp directory that outlives the test is harmless. + } + } + + /// + /// 🚨 Row F1 + E3 — no seal for the running identity, no green build: a push still imports AT + /// the pushed commit. The published root carries a sealed publication of THIS repository at + /// the very pushed commit — but under another identity (a newer line this process does not run). + /// The precondition is asserted through the production reader, so the test cannot pass on a root + /// it never read: this identity has nothing sealed, and the other identity's seal is really + /// there. No workflow_run is delivered at all. + /// + [Fact(Timeout = 180_000)] + public async Task APush_ImportsAtThePushedCommit_WithNoSealForTheRunningIdentity_AndNoGreenBuild() + { + var ct = TestContext.Current.CancellationToken; + StageSeal(OtherIdentity, PushedSha); + var space = await Arrange("NoSeal", ct); + + var (forThisIdentity, outcome) = + SealedPublicationIndex.ReadingFor(publishedRoot, PrebuiltAssemblySeeder.LiveFrameworkMvid); + outcome.Should().NotBe(SealedReadOutcome.Unreadable, "the precondition: the root is readable"); + forThisIdentity.Where(s => s.IsSealed).Should().BeEmpty( + "the premise: NOTHING is sealed for the identity this process runs"); + SealedPublicationIndex.ReadingFor(publishedRoot, OtherIdentity).Sources + .Should().Contain(s => s.IsSealed && s.SourceCommit == PushedSha, + "the control: the seal under the other identity IS there, so 'nothing for this " + + "identity' is a measured reading and not an unread root"); + + repoClient.Trees = repoClient.Trees.SetItem(PushedSha, [Page("Page")]); + var fetched = repoClient.FetchedRefs.Where(r => r == PushedSha) + .Should().Within(TestTimeouts.Convergence * 2) + .Emit("a push imports at once — no seal for this identity and no green build hold it"); + (await Anonymously(() => Webhooks.Process("push", PushPayload(PushedSha)), ct)) + .Should().Be(1, "the one sync source of this repository is selected by the push"); + (await fetched).Should().Be(PushedSha); + + await SourceAt(space, PushedSha, ct); + } + + /// + /// 🚨 Row E3 — a seal for this identity at an OLDER commit neither holds nor redirects a + /// push. The seal exists and is attributable to this repository (asserted through the + /// production reader). The push names a newer commit; the import must fetch THAT commit and + /// never the sealed one. The seal decides only whether prebuilt bytes are adopted. + /// + [Fact(Timeout = 180_000)] + public async Task ASealForAnOlderCommit_NeitherHoldsNorRedirectsThePush() + { + var ct = TestContext.Current.CancellationToken; + StageSeal(PrebuiltAssemblySeeder.LiveFrameworkMvid, OlderSha); + var space = await Arrange("OlderSeal", ct); + + SealedPublicationIndex.ReadingFor(publishedRoot, PrebuiltAssemblySeeder.LiveFrameworkMvid).Sources + .Should().Contain(s => s.IsSealed && s.SourceCommit == OlderSha && s.Repository == RepoFullName, + "the premise: this identity HAS a seal for this repository — at an older commit"); + + repoClient.Trees = repoClient.Trees + .SetItem(OlderSha, [Page("Page")]) + .SetItem(PushedSha, [Page("Page"), Page("Added")]); + var neverTheSeal = repoClient.FetchedRefs.Where(r => r == OlderSha) + .Should().NotEmit(within: TestTimeouts.Convergence, cancellationToken: ct); + var fetched = repoClient.FetchedRefs.Where(r => r == PushedSha) + .Should().Within(TestTimeouts.Convergence * 2) + .Emit("the pushed commit lands; an older seal is no reason to hold or redirect it"); + (await Anonymously(() => Webhooks.Process("push", PushPayload(PushedSha)), ct)).Should().Be(1); + (await fetched).Should().Be(PushedSha); + await neverTheSeal; + + await SourceAt(space, PushedSha, ct); + } + + /// + /// 🚨 Row F4 through the PUSH path — a node created at runtime survives a push import + /// (policy prune-requires-provenance). Push 1 imports two pages; a runtime writer creates a + /// third node in the synced partition; push 2's tree deletes one page. The deleted page is + /// pruned — the positive control, without which "the runtime node survived" could mean the prune + /// never ran — and the runtime node stays. + /// + [Fact(Timeout = 300_000)] + public async Task ARuntimeNode_SurvivesAPushImport_WhileARetiredSourceNodeIsPruned() + { + var ct = TestContext.Current.CancellationToken; + var space = await Arrange("Runtime", ct); + var retired = $"{space}/Retired"; + var runtime = $"{space}/Babysitter"; + + repoClient.Trees = repoClient.Trees + .SetItem(PushedSha, [Page("Kept"), Page("Retired")]) + .SetItem(NextSha, [Page("Kept")]); + + (await Anonymously(() => Webhooks.Process("push", PushPayload(PushedSha)), ct)).Should().Be(1); + await SourceAt(space, PushedSha, ct); + await Children(space, paths => paths.Contains(retired, StringComparer.OrdinalIgnoreCase), ct); + + // The running portal writes state of its own into the synced partition. + IObservable write; + using (Access.SwitchAccessContext(new AccessContext { ObjectId = "pr-babysitter", Name = "PR babysitter" })) + write = MeshService.CreateNode(new MeshNode("Babysitter", space) + { + Name = "Babysitter", + NodeType = "Markdown", + State = MeshNodeState.Active, + Content = new MarkdownContent { Content = "# runtime state" }, + }); + await write.FirstAsync().Timeout(TestTimeouts.Convergence).Await(ct); + + (await Anonymously(() => Webhooks.Process("push", PushPayload(NextSha)), ct)).Should().Be(1); + await SourceAt(space, NextSha, ct); + + var after = await Children(space, + paths => !paths.Contains(retired, StringComparer.OrdinalIgnoreCase), ct); + after.Should().NotContain(retired, + "the POSITIVE control: the repository put Retired there and then deleted it"); + after.Should().Contain(runtime, + "no import manifest records the runtime node, so a push import must never prune it"); + } + + // ── arrangement ────────────────────────────────────────────────────────── + + private static RepoFile Page(string id) => + new($"{id}.md", $"---\nNodeType: Markdown\nName: {id}\n---\n\n# {id}\n"); + + /// <root>/<identity>/plugins/ with the three markers a sealed + /// publication carries. + private void StageSeal(string identity, string commit) + { + var sourceDirectory = Path.Combine(publishedRoot, identity, SealedSourceName); + Directory.CreateDirectory(sourceDirectory); + File.WriteAllText(Path.Combine(sourceDirectory, SealedPublicationIndex.RepositoryMarkerFileName), RepoFullName); + File.WriteAllText(Path.Combine(sourceDirectory, SealedPublicationIndex.SourceCommitMarkerFileName), commit); + File.WriteAllText(Path.Combine(sourceDirectory, ShippedPrebuiltBundles.CompletionSentinelFileName), string.Empty); + } + + private async Task Arrange(string prefix, CancellationToken ct) + { + var space = prefix + Guid.NewGuid().ToString("N")[..8]; + await NodeFactory.CreateNode(new MeshNode(space) + { + NodeType = "Space", + Name = prefix, + State = MeshNodeState.Active, + Content = new Space(), + }).Timeout(TestTimeouts.Convergence).Await(ct); + + var configNode = await Sync + .SaveConfig(space, RepoUrl, "main", null, createBranchIfMissing: false, createRepoIfMissing: false) + .Timeout(TestTimeouts.Convergence).Await(ct); + var syncOwner = configNode.CreatedBy is { Length: > 0 } creator ? creator : UserId; + await Credentials + .Save(syncOwner, new GitHubToken("ghp_test_token", null, "bearer", "repo", null), "octocat") + .Timeout(TestTimeouts.Convergence).Await(ct); + + // The processor selects from an eventually-consistent QUERY of the configs. + await Observable.Interval(50.Milliseconds()).StartWith(0L) + .SelectMany(_ => MeshService + .Query(MeshQueryRequest.FromQuery($"path:{GitHubSyncService.ConfigPath(space)}")) + .Take(1)) + .Where(c => c.Items.Count > 0) + .FirstAsync() + .Timeout(TestTimeouts.Convergence) + .Await(ct); + return space; + } + + /// + /// The import at CONCLUDED ( + /// is written on every conclusion) and LANDED — the source records it as held content. A + /// conclusion that did not land fails here naming the importer's own outcome, never as a bare + /// timeout. + /// + private async Task SourceAt(string space, string sha, CancellationToken ct) + { + var config = await Observable.Interval(50.Milliseconds()).StartWith(0L) + .SelectMany(_ => MeshService + .Query(MeshQueryRequest.FromQuery($"path:{GitHubSyncService.ConfigPath(space)}")) + .Take(1)) + .SelectMany(c => c.Items.Select(n => n.ContentAs(Mesh.JsonSerializerOptions))) + .Where(cfg => cfg is not null && (cfg.LastSyncCommitSha == sha || cfg.LastAttemptedCommitSha == sha)) + .Select(cfg => cfg!) + .FirstAsync() + .Timeout(TestTimeouts.Convergence * 4) + .Await(ct); + config.LastSyncCommitSha.Should().Be(sha, + $"the import at {sha} must land — it concluded '{config.LastSyncOutcome}' " + + $"(note: {config.LastSyncNote ?? "none"})"); + } + + private Task Children(string space, Func until, CancellationToken ct) => + Observable.Interval(100.Milliseconds()).StartWith(0L) + .SelectMany(_ => MeshService + .Query(MeshQueryRequest.FromQuery($"namespace:{space} scope:children")) + .Take(1)) + .Select(c => c.Items.Select(n => n.Path).Where(p => !string.IsNullOrEmpty(p)).ToArray()) + .Where(until) + .FirstAsync() + .Timeout(TestTimeouts.Convergence * 4) + .Await(ct); + + /// The webhook request is ANONYMOUS — its authorization is the HMAC signature — so every + /// ambient identity is dropped and the processor's own System impersonation carries the work. + private async Task Anonymously(Func> work, CancellationToken ct) + { + var accessService = Mesh.ServiceProvider.GetRequiredService(); + accessService.ClearHostIdentity(); + accessService.SetHostIdentity(null); + accessService.SetContext(null); + try + { + return await work().Timeout(TestTimeouts.Convergence).Await(ct); + } + finally + { + accessService.SetHostIdentity(new AccessContext { ObjectId = UserId, Name = TestUsers.Admin.Name }); + } + } + + private static IConfiguration? Materialise(ServiceDescriptor? descriptor, IServiceProvider sp) + => descriptor is null ? null + : descriptor.ImplementationFactory is { } factory ? (IConfiguration)factory(sp) + : descriptor.ImplementationInstance is IConfiguration instance ? instance + : throw new InvalidOperationException( + "The IConfiguration registration is neither a factory nor an instance, so this test " + + "cannot layer onto it."); + + private static JsonElement PushPayload(string afterSha) => JsonDocument.Parse($$""" + { + "ref": "refs/heads/main", + "before": "0123456789abcdef0123456789abcdef01234567", + "after": "{{afterSha}}", + "repository": { "full_name": "{{RepoFullName}}", "default_branch": "main" }, + "commits": [ { "added": ["Page.md"], "modified": [], "removed": [] } ], + "size": 1 + } + """).RootElement; + + /// + /// The GitHub transport: serves a tree per commit and records every commitish fetched. HOT, not + /// replaying, so a NotEmit cannot be handed an earlier step's fetch. + /// + private sealed class TreeRepoClient : IGitHubRepoClient + { + private readonly Subject fetched = new(); + private ImmutableDictionary> trees = + ImmutableDictionary>.Empty; + + public IObservable FetchedRefs => fetched; + + public ImmutableDictionary> Trees + { + get => Volatile.Read(ref trees); + set => Volatile.Write(ref trees, value); + } + + public IObservable Fetch(string repositoryUrl, string commitish, string? subdirectory, string accessToken) + => Observable.Defer(() => + { + fetched.OnNext(commitish); + return Trees.TryGetValue(commitish, out var files) + ? Observable.Return(new RepoSnapshot(commitish, files)) + : Observable.Throw(new InvalidOperationException( + $"the test repository has no tree at {commitish}")); + }); + + public IObservable GetHeadSha(string repositoryUrl, string commitish, string accessToken) + => Observable.Return(PushedSha); + + public IObservable Push(GitHubPushRequest request) => NotUsed(); + public IObservable CreateBranch(GitHubCreateBranchRequest request) => NotUsed(); + public IObservable OpenPullRequest(GitHubOpenPullRequestRequest request) => NotUsed(); + public IObservable GetPullRequestStatus(string repositoryUrl, int number, string accessToken) => NotUsed(); + public IObservable> ListIssues(string repositoryUrl, GitHubIssueState? state, string accessToken) => NotUsed>(); + public IObservable GetIssue(string repositoryUrl, int number, string accessToken) => NotUsed(); + public IObservable CreateIssue(GitHubCreateIssueRequest request) => NotUsed(); + public IObservable CommentIssue(string repositoryUrl, int number, string body, string accessToken) => NotUsed(); + public IObservable SetIssueState(string repositoryUrl, int number, GitHubIssueState state, string accessToken) => NotUsed(); + public IObservable> ListPullRequests(string repositoryUrl, PullRequestStatus? state, string accessToken) => NotUsed>(); + public IObservable GetPullRequestDetail(string repositoryUrl, int number, string accessToken) => NotUsed(); + public IObservable CommentPullRequest(string repositoryUrl, int number, string body, string accessToken) => NotUsed(); + public IObservable MergePullRequest(GitHubMergePullRequestRequest request) => NotUsed(); + + private static IObservable NotUsed() => Observable.Throw( + new NotSupportedException("ModuleUpdateLadderSyncTest's repo client answers only Fetch and GetHeadSha.")); + } +} From df870b072c29af17d4e18deb5b17c56d42c062be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= <6334612+rbuergi@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:28:51 +0200 Subject: [PATCH 2/3] test(ladder): no null-forgiving in the sync ladder; the repo stub resolves only the configured branch Review of #6140: UserId throws on a missing fixture ObjectId, SourceAt filters with OfType instead of cfg!, and GetHeadSha answers only 'main' and fails loudly on any other commitish. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ModuleUpdateLadderSyncTest.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs b/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs index d8ea47bfb4..4b6135baf6 100644 --- a/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs +++ b/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs @@ -60,7 +60,8 @@ public class ModuleUpdateLadderSyncTest(ITestOutputHelper output) : MonolithMesh private readonly string publishedRoot = Path.Combine( Path.GetTempPath(), "mw-ladder-sync-" + Guid.NewGuid().ToString("N")[..12]); - private static string UserId => TestUsers.Admin.ObjectId!; + private static string UserId => TestUsers.Admin.ObjectId + ?? throw new InvalidOperationException("the fixture contract: TestUsers.Admin carries an ObjectId"); private GitHubSyncService Sync => Mesh.ServiceProvider.GetRequiredService(); private GitHubCredentialService Credentials => Mesh.ServiceProvider.GetRequiredService(); @@ -285,8 +286,8 @@ private async Task SourceAt(string space, string sha, CancellationToken ct) .Query(MeshQueryRequest.FromQuery($"path:{GitHubSyncService.ConfigPath(space)}")) .Take(1)) .SelectMany(c => c.Items.Select(n => n.ContentAs(Mesh.JsonSerializerOptions))) - .Where(cfg => cfg is not null && (cfg.LastSyncCommitSha == sha || cfg.LastAttemptedCommitSha == sha)) - .Select(cfg => cfg!) + .OfType() + .Where(cfg => cfg.LastSyncCommitSha == sha || cfg.LastAttemptedCommitSha == sha) .FirstAsync() .Timeout(TestTimeouts.Convergence * 4) .Await(ct); @@ -371,8 +372,13 @@ public IObservable Fetch(string repositoryUrl, string commitish, s $"the test repository has no tree at {commitish}")); }); + /// The configured branch's head is ; any other commitish is a + /// dependency these tests did not declare, so it fails loudly instead of answering. public IObservable GetHeadSha(string repositoryUrl, string commitish, string accessToken) - => Observable.Return(PushedSha); + => commitish == "main" + ? Observable.Return(PushedSha) + : Observable.Throw(new InvalidOperationException( + $"the test repository resolves only the configured branch 'main', not '{commitish}'")); public IObservable Push(GitHubPushRequest request) => NotUsed(); public IObservable CreateBranch(GitHubCreateBranchRequest request) => NotUsed(); From 1be8ee972fd66ffac55ed483400a45ec7af3232b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= <6334612+rbuergi@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:13:17 +0200 Subject: [PATCH 3/3] test(ladder): row E3's 'never the sealed commit' is bounded by the import, not a 90 s NotEmit The NotEmit(within: Convergence) idled ~90 s on CI (the test ran 108 s) and pushed MeshWeaver.Hosting.Test part 1/2 past shard 0's 8-minute cap (exit=124, run 37309830564). Every fetch is now recorded from before the push until the source has landed at the pushed commit; the sealed commit must be absent from that record, and the pushed commit present (the control that the recording saw the import). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ModuleUpdateLadderSyncTest.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs b/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs index 4b6135baf6..9bd65744e3 100644 --- a/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs +++ b/test/MeshWeaver.Hosting.Test/ModuleUpdateLadderSyncTest.cs @@ -168,16 +168,24 @@ public async Task ASealForAnOlderCommit_NeitherHoldsNorRedirectsThePush() repoClient.Trees = repoClient.Trees .SetItem(OlderSha, [Page("Page")]) .SetItem(PushedSha, [Page("Page"), Page("Added")]); - var neverTheSeal = repoClient.FetchedRefs.Where(r => r == OlderSha) - .Should().NotEmit(within: TestTimeouts.Convergence, cancellationToken: ct); + // "Never the sealed commit" is bounded by the import itself, not by a time window: every + // fetch is recorded from before the push until the source has LANDED at the pushed commit, + // and a redirect to the seal could only happen inside that import. A NotEmit over a full + // Convergence window spent ~90 s of CI idling here (108 s for this one test) and pushed its + // Hosting.Test part past the shard's wall-clock cap. + var everyFetch = ImmutableList.Empty; + using var recording = repoClient.FetchedRefs.Subscribe(r => ImmutableInterlocked.Update(ref everyFetch, l => l.Add(r))); var fetched = repoClient.FetchedRefs.Where(r => r == PushedSha) .Should().Within(TestTimeouts.Convergence * 2) .Emit("the pushed commit lands; an older seal is no reason to hold or redirect it"); (await Anonymously(() => Webhooks.Process("push", PushPayload(PushedSha)), ct)).Should().Be(1); (await fetched).Should().Be(PushedSha); - await neverTheSeal; await SourceAt(space, PushedSha, ct); + Volatile.Read(ref everyFetch).Should().NotContain(r => r == OlderSha, + "an older seal decides only whether prebuilt bytes are adopted — the import never fetches it"); + Volatile.Read(ref everyFetch).Should().Contain(r => r == PushedSha, + "the control: the recording saw the import's own fetch, so the absence above is measured"); } ///