Skip to content

feat(testing): Tests areas stream per-case progress, bound every case, and get a console (memex tests) - #5755

Merged
meshweaver-cloud[bot] merged 5 commits into
mainfrom
feat/streaming-tests-runner
Sep 26, 2026
Merged

meshweaver-cloud[bot] merged 5 commits into
mainfrom
feat/streaming-tests-runner

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Why

The maintainer's verification page Admin/Maintenance/refresh-app-tiles-20260828-mainnode-3/Tests (memex-cloud) sat on "Rendering …". A Tests area runs every case and rendered one frame, at the end. So a case that calls an external service live, or one that hangs, looks the same as a stuck page, and the page does not name the case. "Don't we have a proper test runner?"

Measured on memex-cloud (pod memex-portal-deployment-6d84f5d754-xkfk4, 2026-09-26 ~10:52Z), reading the Store/Maintenance cases directly:

  • 44 pure cases: 2 ms total, 0 failed.
  • PaymentPath_Live…: 346 ms (TEST-mode endpoint, verdict Delivering).
  • ModulePins_Live…: 24.3 s, and 17.8 s of it is one read: the portal fetching its own registry's bundle index. Its budget is 20 s. I filed this into triage as rbuergi/Feedback/self-registry-bundle-index-18s-20260926T1108Z.
  • The two Maintenance live cases write real request nodes and wait for the watcher.

So the page's single frame arrives only after tens of seconds of sequential live work. A second finding: every render re-runs the suite. Two get …/area/Tests reads 34 s apart each filed a new pair of Admin/Maintenance/mnt-… request nodes (10:51:00Z, 10:51:34Z), and the second read answered in 0.9 s with the verdict an EARLIER subscription had cached. A console built by polling the area would therefore re-run live cases on every poll.

What

  • MeshTestRunner streams. The first frame shows every case ⏳ pending. After that, one frame per second while a case runs: the running case ▶ with its elapsed time and each output line as it is written, and finished cases ✔/✖. Then one verdict frame (<suite> tests — N/M passed, ✅/❌/⏭ plus a localized word). Rows are Controls.DataGrid + PropertyColumnControl<string>. Titles and words are localized and resolved once in the render scope. Elapsed is formatted invariant.
  • Progress frames can never be read as a verdict. They carry AreaFrameClassifier.TestsRunningId (included in IsTransientFrame), and they never contain ✅/❌ or "N/M passed".
  • Every case is bounded, and a late writer cannot pollute another row. MeshTestCase + Area(host, suite, cases) is for static-method suites. Synchronous bodies run on the Tests I/O pool; if one times out, the verdict names it as still holding its slot. Each case's writer closes at its verdict. The class-wide [MeshFact] writer admits only the running case's lines (an AsyncLocal token).
  • MeshOperations.RunTests(path) runs the area as an activity. It holds ONE subscription and writes a line per case plus its output to an activity in the caller's partition (category TestRun). The terminal status is the verdict (Succeeded or Failed), and on timeout the last line names the case still running. It answers {status: Dispatched, activityPath} at once. Mesh.Operations now references MeshWeaver.GitSync to reuse ActivityRunner.RunActivity.
  • TestsAreaFrame (Layout) is the pure reader of a Tests frame, keyed by class + case.
  • Gate: AreaProbe treats a progress frame as transient. A cut-off run names its progress.
  • Console: memex tests @<node> POSTs api/mesh/run-tests, then polls the ACTIVITY every 2 s and prints each new line. Every request is bounded by --timeout. Exits 0 when all passed, 1 on a failure, 4 when there is no terminal status in time.
  • Doc: Architecture/WritingTests.md has a new section. DecentralisedTests.md Lane A is updated.

Tests (Release -warnaserror, all clean)

  • MeshTestRunnerTests 5/5, including Progress_streams_pending_running_output_and_one_verdict_last.
  • RunTestsStreamsIntoAnActivityTest 1/1: a monolith mesh renders a streaming Tests area, and RunTests turns it into a Failed activity with the running line (with output), the ✅ and ❌ lines, and 1/2 passed.
  • TestsAreaFrameTest 2/2. TestsCommandTest 2/2. AreaProbeTest 7/7. LocalizationTest 66/66.

Cross-repo

This PR is additive only. Nothing public is removed.

Pairs-with: none — additive only; no public type or member leaves src/.

Mirror-sync: tracked for the Plugins React mirror. Run npm run sync:i18n -- --ref <merged core sha> in MeshWeaver.Plugins after this merges. The new keys are activity.tests.{title,case,caseOutput,summary}, tests.progress, tests.column.{class,case,result,time,output} and tests.result.{passed,failed,skipped}.

Follow-ups in MeshWeaver.Plugins, landing after this is in a sealed set and on the portals:

  • Systemorph/MeshWeaver.Plugins#2400: the Store TestsRun onto this runner. This is the page the maintainer saw.
  • The MCP tool run_tests, which calls MeshOperations.RunTests. It is in the same draft PR. The REST route POST api/mesh/run-tests is here in core (MeshApiEndpoints, Bearer-only, pinned in MeshApiCookieAuthTest).

Addresses to recycle after deploy: none on the core side.

🤖 Generated with Claude Code

…ase, and gets a console

A node's Tests area ran every case and rendered ONE frame at the end, so a case that called an
external service live (or hung) left the page on "Rendering …" for the whole suite, with nothing
to tell slow from stuck and nothing naming the case.

- MeshTestRunner streams: every case pending at once, the running case with its ticking elapsed
  time and its output lines as written, finished cases ✔/✖ — then ONE verdict frame (✅/❌,
  "N/M passed"). Progress frames carry AreaFrameClassifier.TestsRunningId and never the verdict
  glyphs, so no consumer can read one as a verdict.
- MeshTestCase + Area(host, suite, cases): the listed-cases shape (static methods) runs through
  the same streaming runner; synchronous bodies run on the Tests I/O pool, so a body that never
  returns fails "timed out: no verdict within Ns" instead of freezing the render.
- AreaProbe (the plugin gate) treats a progress frame as transient; a cut-off run names its progress.
- memex tests <path>: reads the area until the verdict, printing each case as it changes.
- Column titles and progress title localized (en/de).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 26, 2026 11:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical CLI polling and per-case output attribution issues can prevent verdicts and misreport results.

Review effort: Lite
Findings: 4 High severity · 2 Medium severity · 7 Low severity

Open (13)
What changed in this PR

Adds streamed Tests-area progress, bounded test cases, gate handling, localization, and a memex tests console command.

Changes:

  • Streams pending, running, output, and final verdict frames.
  • Adds listed synchronous/live test-case APIs.
  • Updates CLI support, tests, localization, and documentation.
File Description
tools/​MeshWeaver.PluginTester/​AreaProbe.cs Treats progress frames as transient
test/​MeshWeaver.Testing.InMesh.Test/​MeshTestRunnerTests.cs Tests runner progress behavior
test/​MeshWeaver.PluginTester.Test/​AreaProbeTest.cs Tests gate classification
test/​MeshWeaver.Cli.Test/​TestsCommandTest.cs Tests CLI parsing and output
src/​MeshWeaver.Testing.InMesh/​MeshTestRunner.Progress.cs Implements progress snapshots and rendering
src/​MeshWeaver.Testing.InMesh/​MeshTestRunner.cs Integrates streaming execution
src/​MeshWeaver.Testing.InMesh/​MeshTestCase.cs Adds listed test-case definitions
src/​MeshWeaver.Messaging.Hub/​Localization/​strings.en.json Adds English labels
src/​MeshWeaver.Messaging.Hub/​Localization/​strings.de.json Adds German labels
src/​MeshWeaver.Layout/​AreaFrameClassifier.cs Classifies Tests progress frames
src/​MeshWeaver.Documentation/​Data/​Architecture/​WritingTests.md Documents streaming Tests areas
src/​MeshWeaver.Documentation/​Data/​Architecture/​DecentralisedTests.md Updates gate documentation
src/​MeshWeaver.Cli/​TestsCommand.cs Adds console polling and parsing
src/​MeshWeaver.Cli/​Program.cs Registers the tests command

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/MeshWeaver.Cli/TestsCommand.cs Outdated
Comment on lines +28 to +33
/// <summary>One case as the area renders it.</summary>
/// <param name="Case">The case's name.</param>
/// <param name="Result">Its status glyph / verdict.</param>
/// <param name="Time">Its elapsed time.</param>
/// <param name="Output">Its failure message and output lines.</param>
public sealed record Row(string Case, string Result, string Time, string Output);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 069e63f. The CLI no longer tracks rows; it prints the run's activity lines. The reader that now tracks rows (TestsAreaFrame, used by RunTests) keys by class + case (Row.Key). TestsAreaFrameTest pins two classes' same-named cases staying apart.

Comment thread src/MeshWeaver.Cli/TestsCommand.cs Outdated
string? lastTitle = null;
while (true)
{
var body = await client.Get(area, ct);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and measured on memex-cloud: two get reads 34 s apart each filed a new pair of the Maintenance suite's live request nodes, and the second read answered with an earlier subscription's cached verdict. In 069e63f, MeshOperations.RunTests holds ONE area subscription for the whole run and writes it to an activity. memex tests now POSTs api/mesh/run-tests and polls the ACTIVITY, which starts nothing. Pinned end to end by RunTestsStreamsIntoAnActivityTest.

Comment on lines +194 to +195
var finished = run(() => signals.OnNext(new StartedEvent()), line => signals.OnNext(new LineEvent(line)))
.Select(result => (RunEvent)new FinishedEvent(result));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 069e63f. A listed case's writer is closed at its verdict (OutputLines.Close), so a body that outlives its bound cannot write onto the next row. In the fold, a line arriving with no running case is dropped.

Comment on lines +104 to +106
// Every line a case writes lands in its verdict's detail AND streams to the progress
// frame the moment it is written — a slow case shows what it is doing while it does it.
var context = host is null ? null : new MeshTestContext(host, partition, line => { output.Add(line); onLine(line); }, deadline);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 069e63f. The class-wide writer now admits only the RUNNING case's lines. Each case enters a token (AsyncLocal, which flows into its continuations) inside its pool leaf, and the gate is closed at the case's terminal. A leaked case's late lines carry its own token and are dropped instead of landing in the next case's detail or row.

Comment thread src/MeshWeaver.Cli/TestsCommand.cs Outdated
string? lastTitle = null;
while (true)
{
var body = await client.Get(area, ct);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 069e63f. --timeout is now a linked CancellationTokenSource with CancelAfter, passed to every request (the run-tests POST and each activity read) and to the delay. An elapsed deadline maps to exit 4, and the user's own Ctrl-C is kept distinct.

/// <param name="interval">How often to read the area.</param>
/// <param name="output">Where the console goes.</param>
/// <param name="ct">Cancels the wait.</param>
public static async Task<int> Run(MemexClient client, string path, TimeSpan timeout, TimeSpan interval, TextWriter output, CancellationToken ct)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer as on Program.cs: this is the CLI executable's boundary, an HTTP client process with no hub or turn scheduler. It matches the existing async Run helper that every memex verb goes through.

Comment thread src/MeshWeaver.Cli/TestsCommand.cs Outdated
public static async Task<int> Run(MemexClient client, string path, TimeSpan timeout, TimeSpan interval, TextWriter output, CancellationToken ct)
{
var area = $"@{path.TrimStart('@').TrimEnd('/')}/area/Tests";
var printed = new Dictionary<string, Row>(StringComparer.Ordinal);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 069e63f. The CLI now carries only an int (lines printed). The row-change state lives in TestsAreaFrame.Changes, which takes and returns an ImmutableDictionary.

Comment thread src/MeshWeaver.Cli/TestsCommand.cs Outdated
var body = await client.Get(area, ct);
if (body.StartsWith("Error:", StringComparison.Ordinal) || body.StartsWith("Not found", StringComparison.Ordinal))
{
await output.WriteLineAsync(body);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the two async threads above: the awaits are in the console executable (HTTP client, no mesh scheduler), consistent with the existing Run helper in Program.cs.

Comment on lines +135 to +137
CaseResult Verdict(string? failure) => failure is null
? new CaseResult(suite, c.Name, "✅ pass", output.Joined, DateTimeOffset.UtcNow - started)
: new CaseResult(suite, c.Name, "❌ FAIL", failure + (output.Joined.Length > 0 ? " · " + output.Joined : ""), DateTimeOffset.UtcNow - started);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 069e63f. The rendered verdict word beside each glyph is localized: tests.result.passed / failed / skipped, en and de, resolved once in the render scope (ColumnTitles.Display). The stored CaseResult keeps the glyph, which is the contract every reader (gate, RunTests, tests) matches. The verdict TITLE stays English on purpose, because its 'N/M passed' is the plugin gate's parse contract.

.WithId(AreaFrameClassifier.TestsRunningId);

private static CaseRow Row(CaseResult r, string result, string detail) =>
new(r.Class, r.Name, result, IsUnfinished(r) && r.Result == PendingResult ? "" : $"{r.Elapsed.TotalSeconds:0.0}s", detail);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 069e63f. Elapsed is now formatted with CultureInfo.InvariantCulture, and the progress title already was.

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

460 tests  +1   460 ✅ +1   44s ⏱️ -13s
  3 suites ±0     0 💤 ±0 
  3 files   ±0     0 ❌ ±0 

Results for commit 1acc7c1. ± Comparison against base commit cdda28d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files  ±0    1 suites  ±0   3m 10s ⏱️ -2s
348 tests ±0  348 ✅ ±0  0 💤 ±0  0 ❌ ±0 
352 runs  ±0  352 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 1acc7c1. ± Comparison against base commit cdda28d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 690 tests  ±0   1 690 ✅ ±0   4m 14s ⏱️ +3s
    2 suites ±0       0 💤 ±0 
    2 files   ±0       0 ❌ ±0 

Results for commit 1acc7c1. ± Comparison against base commit cdda28d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    3 files  ±0      3 suites  ±0   6m 18s ⏱️ ±0s
2 117 tests +3  2 117 ✅ +3  0 💤 ±0  0 ❌ ±0 
2 118 runs  +3  2 118 ✅ +3  0 💤 ±0  0 ❌ ±0 

Results for commit 1acc7c1. ± Comparison against base commit cdda28d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

752 tests  ±0   560 ✅ ±0   7m 5s ⏱️ -2s
  3 suites ±0   192 💤 ±0 
  3 files   ±0     0 ❌ ±0 

Results for commit 1acc7c1. ± Comparison against base commit cdda28d.

♻️ This comment has been updated with latest results.

rbuergi and others added 3 commits September 26, 2026 13:21
…on, sync-body leaks, i18n, culture)

- MeshOperations.RunTests(path): holds ONE subscription to the node's Tests area and writes it to
  an activity in the caller's partition (category TestRun) — a line per case, its output, and a
  terminal status that is the verdict. Polling get @node/area/Tests re-runs the suite on every read
  (measured on memex-cloud: each read filed the Maintenance suite's live request nodes again and
  answered with an earlier subscription's cached verdict).
- TestsAreaFrame (MeshWeaver.Layout): the pure reader of a Tests area frame, keyed by class+case.
- memex tests: POST api/mesh/run-tests, then polls the ACTIVITY (starts nothing); every request
  bounded by --timeout; immutable state.
- Runner: a case's writer is closed at its verdict, and the class-wide writer admits only the
  running case's lines (AsyncLocal token), so a case that outlived its bound cannot write onto the
  next row; a timed-out synchronous listed body is named as still holding its pool slot; verdict
  words localized beside the glyphs; elapsed formatted invariant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rer-only: it executes)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…live

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    5 files  ±0      5 suites  ±0   14m 37s ⏱️ +47s
4 241 tests +3  4 239 ✅ +3  2 💤 ±0  0 ❌ ±0 
4 245 runs  +3  4 243 ✅ +3  2 💤 ±0  0 ❌ ±0 

Results for commit 1acc7c1. ± Comparison against base commit cdda28d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   17 files  ±0     17 suites  ±0   36m 12s ⏱️ +34s
9 608 tests +7  9 414 ✅ +7  194 💤 ±0  0 ❌ ±0 
9 617 runs  +7  9 423 ✅ +7  194 💤 ±0  0 ❌ ±0 

Results for commit 1acc7c1. ± Comparison against base commit cdda28d.

♻️ This comment has been updated with latest results.

…caseOutput/summary)

The unkeyed-LogMessage ratchet refused the three verbatim lines. Case lines and the verdict
summary are now catalog-keyed (en/de); a failing verdict's own words ride the failure line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 26, 2026
Merged via the queue into main with commit 45e7f94 Sep 26, 2026
43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants