From aea4e9e46b0c2af63eea341aa277900a601f2f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= <6334612+rbuergi@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:10:20 +0200 Subject: [PATCH] fix: a cell finds its own runs without the last-execution stamp (#3301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stamp is one write to one node — it lands whole or not at all — so a cell whose stamp failed (#3249) reads back as NeverRun and is indistinguishable from one nobody ever ran. #3300 established that recovering that ON THE CELL is impossible from the dispatching hub: every candidate is a second write to the node whose first write just failed. It said nothing about the READER, because the reader's fix is not a write. The run is not lost. HandleExecuteScript creates the Activity node BEFORE it dispatches and stamps the originating cell onto ActivityLog.HubPath, so the cell -> run edge is durable; nothing followed it back. CodeRunHistory.ResolveOutputCurrency does, beside the currency rule it extends: OutputCurrency() first (anything but NeverRun costs no query), and only on NeverRun one listing for an Activity naming this cell -> Unverified. A recovered run is never Current: the Activity records THAT the cell ran, never what it ran. No hit leaves NeverRun exactly as today. Two cross-assembly agreements hold the lookup together and no compiler sees either — the nodeType it filters on and the {viewer} sentinel it expands are declared in assemblies Mesh.Contract sits below. CodeRunHistoryTest pins the first by equality and the second by driving CodeNodeType.ResolveActivityParent; the dispatcher now spells the sentinel through the shared constant. Co-Authored-By: Claude Opus 5 (1M context) --- .../Data/Architecture/ScriptExecution.md | 31 +++- ...9-05-a-cell-can-find-its-own-runs-again.md | 43 +++++ .../Configuration/CodeNodeType.cs | 7 +- .../CodeOutputCurrency.cs | 166 ++++++++++++++++++ .../CodeCellCurrencyThroughTheMeshTest.cs | 107 +++++++++++ .../CodeRunHistoryTest.cs | 145 +++++++++++++++ 6 files changed, 496 insertions(+), 3 deletions(-) create mode 100644 src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-05-a-cell-can-find-its-own-runs-again.md create mode 100644 test/MeshWeaver.Graph.Test/CodeRunHistoryTest.cs diff --git a/src/MeshWeaver.Documentation/Data/Architecture/ScriptExecution.md b/src/MeshWeaver.Documentation/Data/Architecture/ScriptExecution.md index 63c010b517..1a1dfe5b5b 100644 --- a/src/MeshWeaver.Documentation/Data/Architecture/ScriptExecution.md +++ b/src/MeshWeaver.Documentation/Data/Architecture/ScriptExecution.md @@ -161,7 +161,36 @@ Two things make that an honest answer rather than a shrug. `Error` rather than `Warning` is deliberate and costs nothing extra: both levels ship to Loki and the path is exceptional, so the only thing that changes is that it trips error-rate alerting. That is the right classification — nothing retries the write, the loss is permanent, and the visible consequence is a cell that tells a human it has never run. "Degraded but self-correcting" is what `Warning` claims, and none of it holds here. -What is still **not** covered: a viewer looking at a cell after a reload cannot tell `NeverRun` from "ran, not recorded". Closing that needs a surface that finds a cell's runs by `ActivityLog.HubPath` rather than by the stamp — a read that does not depend on the stamp, not another write. See [issue #3301](https://github.com/Systemorph/MeshWeaver/issues/3301). +#### Recovering the run for the READER — follow the edge, do not write another marker + +The three candidates above all fail for the same reason: they are **writes**. What the reader needs is a **read**, and the edge to read along already exists. `CodeRunHistory.ResolveOutputCurrency` (`MeshWeaver.Mesh.Contract`, beside the currency rule it extends) walks it: + +```csharp +// The verdict a cell can substantiate when its own stamp cannot supply one. +// Reactive like every other read — one verdict, then completion. +code.ResolveOutputCurrency(cellPath, viewerHome, meshService) + .Subscribe(verdict => { /* NeverRun · Unverified · Stale · Current */ }); +``` + +| Step | What happens | Cost | +|---|---|---| +| 1 | `OutputCurrency()` — the stamp. Anything but `NeverRun` is returned unchanged. | **no query** | +| 2 | Only on `NeverRun`: one listing for an Activity naming this cell on `HubPath`. A hit ⇒ `Unverified`. | one row | +| 3 | No hit ⇒ `NeverRun`, exactly as today. | — | + +Three properties are what make this the right shape rather than merely a working one. + +**It is a listing by predicate, not a point read.** `Query` is eventually consistent, so it may never decide what a *specific known node* contains — that stays `GetMeshNodeStream`'s job. Here a stale negative is harmless by construction: the worst outcome is the answer the cell already gives today. + +**A recovered run is `Unverified`, never `Current`.** The Activity records *that* the cell ran, never *what* it ran — no fingerprint travels with it. That is the same evidence a stamp which landed without its hash leaves, so it gets the same fail-closed verdict, for the same reason. + +**It runs only where it can change the outcome.** A notebook of cells that ran normally costs zero queries, because step 1 answers first. This is the whole reason the lookup hangs off `NeverRun` rather than being a general "find this cell's runs" call every view makes. + +**Where it looks.** The run lands under `{activityParent}/_Activity`, and `CodeNodeType.ResolveActivityParent` picks that parent in three layers: the Code node's own `CodeConfiguration.ActivityParentPath`, then the partition's `PartitionDefinition.DefaultActivityParentPath`, then the partition root — with the `{viewer}` sentinel at either configured layer expanding to the calling user's home. `CodeRunHistory.ActivityNamespaces` reconstructs the layers it can see from the cell alone — the node's own `ActivityParentPath` with `{viewer}` expanded, the partition root, and the reading viewer's home — deduplicated into a single union query, usually one namespace. A partition-level `DefaultActivityParentPath` pointing somewhere else is **not** covered: that lookup is a live query on the partition registry, which `MeshWeaver.Mesh.Contract` sits below. Where it applies the listing finds nothing and the verdict falls back to `NeverRun` — the answer the cell gives today, so the gap costs nothing that was already working. A caller that has already resolved the parent (the dispatcher has, via `CodeNodeType.ResolveActivityParent`) skips the derivation and calls `CodeRunHistory.RunsQuery` with the namespace it resolved. + +🚨 **Two cross-assembly agreements hold this together, and neither is visible to a compiler.** The lookup filters on `nodeType:Activity` and expands the `{viewer}` sentinel — both declared in assemblies it sits *below* (`GraphNodeTypeNames.Activity`, `CodeNodeType.ResolveActivityParent`). A rename on either side would leave the query looking for something nothing writes: zero rows, every recovered cell silently back to `NeverRun`, no compiler error anywhere. `CodeRunHistoryTest` pins both — the node-type name by equality, the sentinel by *driving the dispatcher's own resolver* and requiring the answer the lookup assumes. + +**What remains uncovered.** The lookup answers *whether* the cell ran, not *what it produced*: it deliberately does not re-point the output pane at the recovered activity. And an operator still gets the `Error` under event id `3249` — the stamp failing is a real fault, and a reader who can no longer see the consequence is not a reason to stop reporting it. ### 2. From application code — `SubmitCodeRequest` directly diff --git a/src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-05-a-cell-can-find-its-own-runs-again.md b/src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-05-a-cell-can-find-its-own-runs-again.md new file mode 100644 index 0000000000..7f06040197 --- /dev/null +++ b/src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-05-a-cell-can-find-its-own-runs-again.md @@ -0,0 +1,43 @@ +--- +Name: A cell that ran no longer claims it never did +Category: Fix +Description: When the bookkeeping write that records a script run failed, the cell reported itself as never run after the next page load — even though the run had happened and its log was still there. The cell now finds the run itself, and says its output is unverified rather than pretending there is none. +Icon: History +Order: -20260905 +--- + +# A cell that ran no longer claims it never did + +Press **Run** on a code cell and two separate things are written. The run itself gets its own +record — the activity, with its log, its status and everything the script printed. Separately, a +short note is written back onto the cell: *this cell was run, at this time, by this person, and +here is where the log lives*. + +That second note is what the cell reads on your next visit. It is one small write, and very +occasionally it does not land — a busy moment at startup, a refused write, content the platform +cannot read back. When that happened, the note was simply absent, and the cell had nothing left to +tell you with. + +So it told you the only thing it could see: **nothing had ever run here.** + +That was wrong, and it was wrong in the least helpful direction. The run had happened. Its log was +still sitting in your activity list, complete. The only thing missing was the cell's pointer to it — +and the cell reported that missing pointer as a missing run. + +## What happens now + +The cell looks for its own runs instead of relying on the note. Every run writes down which cell it +came from, before it starts, so the trail back exists whether or not the note was ever written. + +- A cell whose note is intact behaves exactly as before — nothing extra happens, and nothing got + slower. +- A cell whose note went missing now finds the run and says so, marking its output **unverified**: + the run is real, but nothing recorded which version of the code it ran, so the cell will not + claim the output matches what you are looking at. +- A cell nobody has ever run still says nothing at all. An unrun cell has no output to be wrong + about, and it should not carry a warning. + +"Unverified" rather than "up to date" is deliberate. The recovered run proves *that* the cell ran; +it does not prove *what* it ran. Telling you the output is current when nothing substantiates that +would be a wrong claim, not merely a missing one — so the cell tells you what it actually knows and +lets you decide whether to run it again. diff --git a/src/MeshWeaver.Graph/Configuration/CodeNodeType.cs b/src/MeshWeaver.Graph/Configuration/CodeNodeType.cs index cb2168dc05..5814bf8720 100644 --- a/src/MeshWeaver.Graph/Configuration/CodeNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/CodeNodeType.cs @@ -152,8 +152,11 @@ public static IObservable ResolveActivityParent( return unresolved switch { null => partitionRoot, - "{viewer}" when !string.IsNullOrEmpty(viewerHome) => viewerHome!, - "{viewer}" => partitionRoot, + // Spelled through the constant, not the literal: the #3301 lookup that finds a + // cell's runs when the stamp did not land has to expand the sentinel THE SAME + // WAY, and two literals in two assemblies are free to drift silently. + CodeRunHistory.ViewerHomeSentinel when !string.IsNullOrEmpty(viewerHome) => viewerHome!, + CodeRunHistory.ViewerHomeSentinel => partitionRoot, var p => p }; }); diff --git a/src/MeshWeaver.Mesh.Contract/CodeOutputCurrency.cs b/src/MeshWeaver.Mesh.Contract/CodeOutputCurrency.cs index 1c5a00a16c..5942453a31 100644 --- a/src/MeshWeaver.Mesh.Contract/CodeOutputCurrency.cs +++ b/src/MeshWeaver.Mesh.Contract/CodeOutputCurrency.cs @@ -1,3 +1,7 @@ +using System.Collections.Immutable; +using System.Reactive.Linq; +using MeshWeaver.Mesh.Services; + namespace MeshWeaver.Mesh; /// @@ -125,3 +129,165 @@ public static CodeOutputCurrency OutputCurrency(this CodeConfiguration? code) public static bool ProvesOutputIsCurrent(this CodeConfiguration? code) => code.OutputCurrency() is CodeOutputCurrency.Current; } + +/// +/// The half of the verdict that does NOT depend on the last-execution stamp (#3301). +/// +/// The gap this closes. A cell reads its own run history from ONE denormalised +/// pointer — the stamp that writes , +/// , +/// and together. When that write does not land +/// (#3249: the workspace not ready, a refused partition write, content that cannot be read as a +/// ) the pointer is simply absent, and after a page reload the cell +/// is indistinguishable from one nobody has ever run. +/// answers — correctly, from the evidence it has — and the +/// reader is told the run never happened. +/// +/// Why a READ is the fix and a write is not. The run is not lost. The dispatcher +/// creates the Activity node BEFORE it dispatches and stamps the originating cell onto it +/// (ActivityLog.HubPath = the Code node's path), so the cell → run edge is durable; it is +/// only missing in the direction the view reads it. Every scheme that recovers it by putting a +/// marker on the CELL is a second write to the node whose first write just failed — recovery in the +/// same failure domain — which is why Doc/Architecture/ScriptExecution → "When the stamp does +/// not land" rules all three out. Following the edge backwards needs no write at all. +/// +/// Why this is a legitimate query. It is a LISTING BY PREDICATE, where a stale negative +/// is harmless: the worst outcome is the answer the cell already gives today. It is emphatically not +/// the forbidden shape — reading one known node's content by path — which stays +/// GetMeshNodeStream's job. +/// +/// And why it is not run on every cell. consults +/// the mesh ONLY where the stamp answers — the one state +/// where the lookup can change the outcome. A notebook of cells that ran normally costs zero +/// queries. +/// +public static class CodeRunHistory +{ + /// + /// The nodeType a run's Activity node carries. Spelled as a literal because + /// MeshWeaver.Mesh.Contract sits BELOW MeshWeaver.Graph.Contract, where + /// GraphNodeTypeNames.Activity declares it — the two are pinned equal by + /// CodeRunHistoryTest.TheActivityNodeTypeNameMatchesTheGraphVocabulary rather than left to + /// agree by luck. + /// + public const string ActivityNodeTypeName = "Activity"; + + /// + /// The sentinel a Code node (or a PartitionDefinition) may put in + /// to mean "the VIEWER's home partition, not + /// mine" — expanded by CodeNodeType.ResolveActivityParent at dispatch, and expanded the + /// same way here so the lookup searches where the run actually landed. + /// + public const string ViewerHomeSentinel = "{viewer}"; + + /// + /// The one spelling of "this cell's own runs": Activity satellites in + /// whose ActivityLog.HubPath is + /// . + /// + /// Ordered newest-first and capped at one row: the question is EXISTENCE, so listing every + /// run of a cell that has run a thousand times would pay for nine hundred and ninety-nine rows + /// nobody reads. + /// + /// Path of the Code node — the value the dispatcher wrote to HubPath. + /// The {parent}/_Activity namespace to look in. + public static string RunsQuery(string cellPath, string activityNamespace) => + $"namespace:{activityNamespace} nodeType:{ActivityNodeTypeName} " + + $"content.hubPath:{cellPath} sort:LastModified-desc limit:1"; + + /// + /// The _Activity namespaces a cell's runs can land in, derived from the data at hand — + /// the cell's own (with + /// expanded), its partition root (the default), and the + /// viewer's home. Deduplicated, so the common case is a single namespace. + /// + /// 🚨 What this deliberately does NOT cover, stated rather than hidden. A + /// PartitionDefinition.DefaultActivityParentPath pointing at a FOURTH place is invisible + /// here: that lookup is a live workspace query on the partition registry, which this assembly + /// sits below. Where it applies, the lookup finds nothing and the verdict falls back to + /// — exactly what the cell says today, so the gap + /// costs nothing that was already working. A caller that has ALREADY resolved the parent (the + /// dispatcher does, through CodeNodeType.ResolveActivityParent) skips this derivation and + /// calls with the namespace it resolved. + /// + /// Path of the Code node. + /// The cell's configuration, for its activity-parent override. + /// The reading viewer's home partition, if known. + public static ImmutableArray ActivityNamespaces( + string? cellPath, CodeConfiguration? code, string? viewerHome) + { + if (string.IsNullOrEmpty(cellPath)) + return []; + + var partitionRoot = cellPath.Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + if (string.IsNullOrEmpty(partitionRoot)) + return []; + + var home = string.IsNullOrEmpty(viewerHome) ? null : viewerHome; + var configured = code?.ActivityParentPath switch + { + null or "" => null, + ViewerHomeSentinel => home ?? partitionRoot, + var p => p, + }; + + return new[] { configured, partitionRoot, home } + .Where(parent => !string.IsNullOrEmpty(parent)) + .Select(parent => $"{parent}/{ActivityNodeGuard.ActivitySegment}") + .Distinct(StringComparer.Ordinal) + .ToImmutableArray(); + } + + /// + /// The cell's currency verdict, recovered from the mesh when its own stamp cannot supply one. + /// + /// + /// first. Anything but + /// is returned unchanged and costs NO query — the + /// stamp is the fast path and stays it. + /// Only on : one listing over the cell's + /// candidate _Activity namespaces for a run that names this cell. A hit means a run + /// happened; nothing about that run says WHAT it ran, so the honest verdict is + /// — the same fail-closed answer as a stamp that + /// landed without its fingerprint, and for the same reason. + /// No hit ⇒ , unchanged. A cell nobody has run + /// still says nothing, and a viewer who cannot SEE the activity (the query is access-filtered + /// like every other) gets the same silence rather than a claim about data they may not read. + /// + /// + /// One-shot by construction. The result is the query's Initial snapshot and + /// then completion — this is a read, not a binding. A live subscription per never-run cell is + /// the cost the issue's own analysis rules out, and a stale negative here is harmless by the + /// same argument that makes the listing legitimate at all. A view keeps its liveness from the + /// node stream it is already bound to: when a later run stamps the cell, the node emits and the + /// caller re-resolves. + /// + /// The cell's configuration. + /// Path of the Code node. + /// The reading viewer's home partition, if known. + /// The mesh's query surface. + /// Exactly one verdict, then completion. + public static IObservable ResolveOutputCurrency( + this CodeConfiguration? code, + string? cellPath, + string? viewerHome, + IMeshService meshService) + { + var stamped = code.OutputCurrency(); + if (stamped is not CodeOutputCurrency.NeverRun) + return Observable.Return(stamped); + + var namespaces = ActivityNamespaces(cellPath, code, viewerHome); + if (namespaces.Length == 0) + return Observable.Return(CodeOutputCurrency.NeverRun); + + return meshService + .Query(MeshQueryRequest.FromQueries( + namespaces.Select(ns => RunsQuery(cellPath!, ns)))) + .Where(change => change.ChangeType is QueryChangeType.Initial) + .Take(1) + .Select(change => change.Items.Count > 0 + ? CodeOutputCurrency.Unverified + : CodeOutputCurrency.NeverRun); + } +} diff --git a/test/MeshWeaver.Graph.Test/CodeCellCurrencyThroughTheMeshTest.cs b/test/MeshWeaver.Graph.Test/CodeCellCurrencyThroughTheMeshTest.cs index 7402f8d0b2..eec4e0ae4a 100644 --- a/test/MeshWeaver.Graph.Test/CodeCellCurrencyThroughTheMeshTest.cs +++ b/test/MeshWeaver.Graph.Test/CodeCellCurrencyThroughTheMeshTest.cs @@ -110,8 +110,115 @@ await workspace.GetMeshNodeStream(path) "the visible output belongs to source the reader is no longer looking at"); } + /// + /// #3301, the whole issue in one case: a run whose last-execution stamp NEVER LANDED is still + /// found, because the Activity node the dispatcher created before dispatching names the cell on + /// ActivityLog.HubPath. + /// + /// The repro is deterministic, not simulated. Two cells really run through + /// ExecuteScriptRequest, so two real Activity nodes exist in the same _Activity + /// namespace. Then one cell's stamp is WIPED — which is exactly the state #3249's failure paths + /// leave behind (the write is one node write; it lands whole or not at all). From that point the + /// cell is, to every reader, indistinguishable from one nobody ever ran. + /// + /// Three arms, because a lookup that always says "ran" would be worthless. The + /// wiped cell must be recovered as ; a cell that + /// genuinely never ran must stay ; and the second, + /// still-stamped cell proves the content.hubPath predicate is doing the work rather than + /// the namespace listing finding any activity at all. + /// + [Fact(Timeout = 180_000)] + public async Task ARunWhoseStampNeverLandedIsStillFoundByTheActivityItWrote() + { + var meshService = Mesh.ServiceProvider.GetRequiredService(); + var options = Mesh.JsonSerializerOptions; + + // Two cells, both genuinely run: the second one's Activity shares the _Activity namespace, + // so a lookup that merely listed the namespace would pass on the wrong evidence. + var wiped = await CreateCell(new CodeConfiguration { Code = Source, IsExecutable = true }); + var neighbour = await CreateCell(new CodeConfiguration { Code = Source, IsExecutable = true }); + await Run(wiped); + await Run(neighbour); + + // The stamp is one write to one node — it lands whole or not at all. Removing it reproduces + // the state every #3249 failure path leaves behind. + await Mesh.GetWorkspace().GetMeshNodeStream(wiped) + .Update(current => current with + { + Content = (current.ContentAs(options) ?? new CodeConfiguration()) + with + { + LastExecutedAt = null, + LastExecutedBy = null, + LastActivityPath = null, + LastExecutedCodeHash = null, + }, + }) + .FirstAsync() + .Timeout(Bound) + .Await(); + + var stampless = await ReadCell(wiped, c => c is + { + Code: Source, LastExecutedAt: null, LastActivityPath: null, LastExecutedCodeHash: null, + }); + + // The defect, asserted rather than assumed: from the cell alone, the run is gone. + stampless.OutputCurrency().Should().Be(CodeOutputCurrency.NeverRun, + "with no stamp the cell carries no evidence of its own run — this is the state a reader " + + "sees after a reload, and it is indistinguishable from a cell nobody ever ran (#3301)"); + + // The fix: the run is found by the edge the DISPATCHER wrote. + var recovered = await stampless + .ResolveOutputCurrency(wiped, viewerHome: null, meshService) + .Timeout(Bound) + .Await(); + recovered.Should().Be(CodeOutputCurrency.Unverified, + "the Activity node created before the dispatch still names this cell on HubPath, so the " + + "run is not lost — only the cell's pointer to it is. A run with nothing recording WHAT " + + "it ran is Unverified, the same fail-closed verdict as a stamp that arrived without " + + "its fingerprint"); + + // Non-vacuity: a cell that genuinely never ran must still say so, or the lookup is a rubber + // stamp that would report every unrun cell in the mesh as having run. + var untouched = await CreateCell(new CodeConfiguration { Code = Source, IsExecutable = true }); + var untouchedCell = await ReadCell(untouched, c => c is { Code: Source }); + var untouchedVerdict = await untouchedCell + .ResolveOutputCurrency(untouched, viewerHome: null, meshService) + .Timeout(Bound) + .Await(); + untouchedVerdict.Should().Be(CodeOutputCurrency.NeverRun, + "no Activity anywhere names this cell — and the neighbour's run, which lives in the very " + + "same _Activity namespace, must not be mistaken for it. A verdict that could not come " + + "out NeverRun here would prove nothing above"); + + // The stamped neighbour never reaches the lookup at all — the stamp answers first, which is + // what keeps a notebook of normal cells at zero queries. + var neighbourCell = await ReadCell(neighbour, c => c is { LastExecutedCodeHash: not null and not "" }); + var neighbourVerdict = await neighbourCell + .ResolveOutputCurrency(neighbour, viewerHome: null, meshService) + .Timeout(Bound) + .Await(); + neighbourVerdict.Should().Be(CodeOutputCurrency.Current, + "a cell whose stamp landed is judged by the stamp, unchanged and without a query"); + } + // ── helpers ── + private async Task Run(string path) + { + var dispatch = await RequestHub + .Observe(new ExecuteScriptRequest(), o => o.WithTarget(new Address(path))) + .FirstAsync() + .Timeout(Bound) + .Await(); + dispatch.Message.Success.Should().BeTrue( + $"the run of '{path}' must be accepted before anything about it can be asserted"); + // The stamp is what makes the run observable from the cell; waiting on it also guarantees + // the Activity node exists before the lookup goes looking for it. + await ReadCell(path, c => c is { LastExecutedCodeHash: not null and not "" }); + } + private async Task CreateCell(CodeConfiguration content) { var id = $"cell{Guid.NewGuid():N}"[..12]; diff --git a/test/MeshWeaver.Graph.Test/CodeRunHistoryTest.cs b/test/MeshWeaver.Graph.Test/CodeRunHistoryTest.cs new file mode 100644 index 0000000000..c0306c86c6 --- /dev/null +++ b/test/MeshWeaver.Graph.Test/CodeRunHistoryTest.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using MeshWeaver.Fixture; +using MeshWeaver.Graph.Configuration; +using MeshWeaver.Mesh; +using Xunit; + +namespace MeshWeaver.Graph.Test; + +/// +/// #3301 — the pure half of "a cell can find its own runs without the last-execution stamp". +/// +/// What is being pinned. follows the cell → run edge that +/// the DISPATCHER writes (ActivityLog.HubPath = the Code node's path) instead of the +/// denormalised pointer the STAMP writes. Two things have to hold for that to work at all, and +/// neither is visible to a compiler: the query has to name the vocabulary the dispatcher actually +/// used, and the {viewer} sentinel has to expand to the same place on both sides. Both are +/// cross-ASSEMBLY agreements — MeshWeaver.Mesh.Contract sits below +/// MeshWeaver.Graph/MeshWeaver.Graph.Contract, so the lookup cannot reference the +/// dispatcher's own constants and would otherwise agree with it only by luck. +/// +/// The through-the-mesh half — that a real run really is found after its stamp is wiped — is +/// . +/// +public class CodeRunHistoryTest +{ + private const string Cell = "acme/notebook/cell-7"; + + /// + /// 🚨 The lookup filters on nodeType:Activity, and that string is DECLARED in + /// MeshWeaver.Graph.Contract — an assembly the lookup cannot reference. A rename there + /// would leave querying a node type nothing writes any + /// more: zero rows, every recovered cell silently back to , + /// and no compiler error anywhere. This test is the only thing that would notice. + /// + [Fact] + public void TheActivityNodeTypeNameMatchesTheGraphVocabulary() => + CodeRunHistory.ActivityNodeTypeName.Should().Be(GraphNodeTypeNames.Activity, + "the lookup's nodeType filter must name the type the dispatcher actually creates — " + + "MeshWeaver.Mesh.Contract cannot reference GraphNodeTypeNames, so this equality is " + + "the only thing holding the two spellings together"); + + /// + /// 🚨 The other cross-assembly agreement, pinned BEHAVIOURALLY rather than by comparing two + /// literals: the sentinel expands must be the + /// one CodeNodeType.ResolveActivityParent expands, and to the same target. Drive the + /// dispatcher's own resolver and require the answer the lookup assumes. + /// + [Fact] + public async Task TheViewerSentinelExpandsTheSameWayOnBothSides() + { + var resolved = await CodeNodeType.ResolveActivityParent( + Observable.Return(null), + CodeRunHistory.ViewerHomeSentinel, + viewerHome: "rbuergi", + partitionRoot: "acme", + lookupBudget: TimeSpan.FromSeconds(5)) + .FirstAsync() + .Timeout(TimeSpan.FromSeconds(10)) + .Await(); + + resolved.Should().Be("rbuergi", + "the dispatcher writes the run into the VIEWER's home when the sentinel is configured, " + + "so a lookup that expanded it to the cell's own partition would search the one place " + + "the activity is not"); + + CodeRunHistory.ActivityNamespaces( + Cell, + new CodeConfiguration { ActivityParentPath = CodeRunHistory.ViewerHomeSentinel }, + viewerHome: "rbuergi") + .Should().Contain($"{resolved}/_Activity", + "the lookup must search exactly where the resolver said the run was written"); + } + + /// + /// The default: no override, so the dispatcher writes to the cell's own partition root and the + /// lookup searches exactly one namespace. The common case must not fan out. + /// + [Fact] + public void WithNoOverrideTheLookupSearchesOnlyTheCellsOwnPartition() => + CodeRunHistory.ActivityNamespaces(Cell, new CodeConfiguration(), viewerHome: null) + .Should().Equal("acme/_Activity"); + + /// + /// A viewer reading someone else's cell adds their own home — the place a {viewer}-routed + /// run would have landed — and nothing else. Two namespaces, one union query. + /// + [Fact] + public void AForeignViewerAlsoSearchesTheirOwnHome() => + CodeRunHistory.ActivityNamespaces(Cell, new CodeConfiguration(), viewerHome: "rbuergi") + .Should().Equal("acme/_Activity", "rbuergi/_Activity"); + + /// + /// The viewer reading a cell in their OWN partition must not produce the same namespace twice — + /// a duplicated query is a duplicated read for an answer that cannot differ. + /// + [Fact] + public void TheViewersOwnPartitionIsNotSearchedTwice() => + CodeRunHistory.ActivityNamespaces(Cell, new CodeConfiguration(), viewerHome: "acme") + .Should().Equal("acme/_Activity"); + + /// + /// An explicit is where the dispatcher writes, + /// so it is searched — and the partition root stays in the set, because runs recorded BEFORE the + /// override was configured are still there and still this cell's. + /// + [Fact] + public void AnExplicitActivityParentIsSearchedAlongsideThePartitionRoot() => + CodeRunHistory.ActivityNamespaces( + Cell, + new CodeConfiguration { ActivityParentPath = "acme/runs" }, + viewerHome: null) + .Should().Equal("acme/runs/_Activity", "acme/_Activity"); + + /// A path that names no partition cannot be looked up — and must not produce a query + /// with an empty namespace, which is the unanchored cross-schema shape a provider refuses. + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("/")] + public void APathThatNamesNoPartitionYieldsNoQuery(string? cellPath) => + CodeRunHistory.ActivityNamespaces(cellPath, new CodeConfiguration(), viewerHome: null) + .Should().BeEmpty(); + + /// + /// The query's shape, spelled out once: the run is found by the edge the DISPATCHER wrote, it is + /// anchored to a namespace (never an unanchored cross-partition scan), and it asks for existence + /// — newest first, one row — not for the cell's whole run history. + /// + [Fact] + public void TheQueryFindsRunsByTheDispatchersEdgeAndAsksOnlyForExistence() + { + var query = CodeRunHistory.RunsQuery(Cell, "acme/_Activity"); + + query.Should().Contain($"content.hubPath:{Cell}", + "HubPath is the cell → run edge the dispatcher stamps onto the Activity node BEFORE it " + + "dispatches — the whole point is that it survives a failed last-execution stamp"); + query.Should().Contain("namespace:acme/_Activity", + "an unanchored query names no partition and is refused rather than fanned out"); + query.Should().Contain("limit:1").And.Contain("sort:LastModified-desc", + "the question is existence, so a cell with a thousand runs must still cost one row"); + } +}