From a7d97b1b85fbfc96641abf1a21f61a76e1c4da5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Mon, 17 Aug 2026 15:35:39 +0200 Subject: [PATCH 1/7] fix(docs-tests): always rebuild dist in producer mode to walk current source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RouteCheckTests fixture previously honored a pre-existing docs/.vitepress/dist/index.html as evidence that dist was current. That was safe under the sharded CI matrix (ROUTE_SHARD_TOTAL > 1), where the docs-prepare job is the docfx-owning producer and each shard downloads its artefact — but not in producer mode. A warm local checkout keeps a stale dist across sessions, and the fixture would silently walk it: the landing-page test asserted against the old rendered HTML (missing the https:// og:image URL that landed in a later config.ts revision), and the producer-mode mtime invariant fired because no rebuild took place. Split the two modes explicitly: consumer shards keep the artefact contract (skip rebuild when index.html is present); producer mode always rebuilds via npm run build. This walks the current source on every run, satisfies the mtime invariant unconditionally in producer mode, and preserves the sharded artefact flow. Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo --- .../RouteCheckTests.cs | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs index daad0c643..524747e26 100644 --- a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs +++ b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs @@ -125,20 +125,39 @@ public async Task OneTimeSetUp() RunNpm("ci", _docsRoot); } - // Rebuild dist only when it is missing so the test asserts against - // a tree generated from the current source. CI's sharded - // route-check jobs download the dist/ tree from the `docs-prepare` - // workflow artefact and do NOT install docfx, so re-running - // `npm run build` here would invoke the `prebuild` hook - // (`scripts/generate-api-ref.sh` → `docfx metadata`) and fail - // with "docfx not found on PATH". Honouring the pre-existing - // dist/index.html sentinel matches the workflow's documented - // contract: docs-prepare is the single docfx-owning producer - // and each shard consumes its artefact. Locally, deleting - // docs/.vitepress/dist/ (or running on a clean clone) still - // triggers a full build. + // Producer vs. consumer mode. + // + // Producer mode (non-shard local + non-shard CI leg): the + // fixture is the sole authority on dist/, so it always + // rebuilds — a warm-cache local run must still walk a tree + // generated from the CURRENT source markdown, current + // config.ts, current sidebar, and so on. Honouring a + // pre-existing dist/index.html sentinel was the earlier + // policy and it silently walked a stale tree whenever a + // developer re-ran `dotnet test` after editing source: the + // walked routes, meta tags, and rendered HTML lagged the + // source by an arbitrary distance (a stale dist from Jun 2 + // failed the landing-page og:image assertion on Aug 17 for + // exactly this reason, because the config-side fix that + // added the https:// og:image URL had landed since the last + // build). The rebuild cost is bounded by npm's incremental + // Vite bundling — a warm-cache no-source-change rebuild is + // seconds, not minutes — and this fixture is already gated + // by [Category("E2E")] so it never blocks the fast tier. + // + // Consumer mode (sharded CI matrix, ROUTE_SHARD_TOTAL > 1): + // CI's sharded route-check jobs download the dist/ tree + // from the `docs-prepare` workflow artefact and do NOT + // install docfx, so re-running `npm run build` would invoke + // the `prebuild` hook (`scripts/generate-api-ref.sh` → + // `docfx metadata`) and fail with "docfx not found on + // PATH". Under shard mode the fixture consumes whatever + // dist/ the artefact download produced; the docs-prepare + // job is the single docfx-owning producer. var distIndex = Path.Combine(distDir, "index.html"); - if (!File.Exists(distIndex)) + var (_, shardTotal) = RouteCheckHelpers.ReadShardEnv(); + var isConsumerShard = shardTotal > 1; + if (!isConsumerShard || !File.Exists(distIndex)) { stage = "npm run build"; RunNpm("run build", _docsRoot); From 35af9ba549fe866f93c08e84fe863f2aafba7af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Mon, 17 Aug 2026 18:48:00 +0200 Subject: [PATCH 2/7] refactor(docs-tests): call vitepress build directly to keep obj/ untouched MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amends the earlier producer-mode-always-rebuilds fix so the fixture no longer routes through npm run build. The `prebuild` hook wired into docs/package.json runs docs/scripts/generate-api-ref.sh, which `dotnet build -c Debug --no-incremental`s every library, agent, adapter and module project. That sweep clobbers each project's obj/project.assets.json down to a Debug-only net8.0 view and races any in-flight multi-TFM Release build under `dotnet test MTConnect.NET.sln -c Release`, tripping NETSDK1005 on every non-net8.0 target MSBuild has not yet linked (net47, net461, net472, net9.0, net10.0, …). Invoke node node_modules/vitepress/bin/vitepress.js build directly under docs/ instead. Walks the same source markdown, produces the same dist/, keeps the producer-mode rebuild guarantee, and leaves the obj/ tree untouched. The docs/api/ sub-tree stays as whatever the last regen produced — the fixture never owned that regen (the docs-prepare workflow and generate-api-ref.sh do). Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo --- .../RouteCheckTests.cs | 109 +++++++++++++++--- 1 file changed, 96 insertions(+), 13 deletions(-) diff --git a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs index 524747e26..b5bc927d3 100644 --- a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs +++ b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs @@ -140,27 +140,40 @@ public async Task OneTimeSetUp() // failed the landing-page og:image assertion on Aug 17 for // exactly this reason, because the config-side fix that // added the https:// og:image URL had landed since the last - // build). The rebuild cost is bounded by npm's incremental - // Vite bundling — a warm-cache no-source-change rebuild is - // seconds, not minutes — and this fixture is already gated - // by [Category("E2E")] so it never blocks the fast tier. + // build). // // Consumer mode (sharded CI matrix, ROUTE_SHARD_TOTAL > 1): // CI's sharded route-check jobs download the dist/ tree - // from the `docs-prepare` workflow artefact and do NOT - // install docfx, so re-running `npm run build` would invoke - // the `prebuild` hook (`scripts/generate-api-ref.sh` → - // `docfx metadata`) and fail with "docfx not found on - // PATH". Under shard mode the fixture consumes whatever - // dist/ the artefact download produced; the docs-prepare - // job is the single docfx-owning producer. + // from the `docs-prepare` workflow artefact and skip the + // rebuild. The docs-prepare job is the single docfx-owning + // producer. + // + // Why call vitepress directly instead of `npm run build`: + // the `prebuild` hook wired into `package.json` runs + // `docs/scripts/generate-api-ref.sh`, which does a + // `dotnet build -c Debug --no-incremental` sweep of every + // library, agent, adapter and module project. Under a full + // `dotnet test MTConnect.NET.sln -c Release` invocation the + // solution build is still in flight (multi-TFM Release + // outputs for net47, net461, net472, net9.0, net10.0, … + // build in parallel with the net8.0 test hosts), so + // clobbering each project's `obj/project.assets.json` back + // to a Debug-only net8.0 view races the Release build and + // trips NETSDK1005 on every non-net8.0 target that MSBuild + // has not yet linked. Invoking vitepress directly walks the + // same source markdown, produces the same dist/, keeps the + // producer-mode rebuild guarantee, and leaves the obj/ + // tree untouched. The api reference sub-tree under + // docs/api/ stays as whatever the last regen produced — + // this fixture does not own that regen (the docs-prepare + // workflow and `docs/scripts/generate-api-ref.sh` do). var distIndex = Path.Combine(distDir, "index.html"); var (_, shardTotal) = RouteCheckHelpers.ReadShardEnv(); var isConsumerShard = shardTotal > 1; if (!isConsumerShard || !File.Exists(distIndex)) { - stage = "npm run build"; - RunNpm("run build", _docsRoot); + stage = "vitepress build"; + RunVitepressBuild(_docsRoot); } // Install the chromium binary the Playwright .NET binding drives. @@ -809,6 +822,76 @@ private static void StopPreviewServer(Process? proc) // ─── npm bootstrap ─────────────────────────────────────────────────────── + /// + /// Invoke the local vitepress binary directly against the docs root, + /// bypassing the package.json prebuild hook that + /// npm run build would trigger. The prebuild step runs + /// docs/scripts/generate-api-ref.sh, which does a + /// dotnet build -c Debug --no-incremental sweep across the + /// entire library, agent, adapter and module surface; that sweep + /// rewrites every touched project's obj/project.assets.json + /// to a Debug-only net8.0 view and races any in-flight + /// multi-TFM Release build (NETSDK1005 on net47, + /// net9.0, net10.0, …). This helper resolves + /// node_modules/vitepress/bin/vitepress.js relative to the + /// docs root, drains stdout+stderr concurrently to avoid the + /// classic pipe-deadlock pattern, and rethrows with the captured + /// output when the child exits non-zero. + /// + /// + /// Absolute path to the docs site (docs/ under the repo + /// root); becomes the child process's working directory and the + /// anchor for the node_modules lookup. + /// + /// + /// Thrown when the vitepress binary cannot be located, + /// returns + /// , or the child process exits with a + /// non-zero code (the captured stdout and stderr are appended to + /// the exception message for diagnosis). + /// + private static void RunVitepressBuild(string docsRoot) + { + var vitepressEntry = Path.Combine(docsRoot, "node_modules", "vitepress", "bin", "vitepress.js"); + if (!File.Exists(vitepressEntry)) + { + throw new InvalidOperationException( + $"Cannot invoke vitepress build directly — expected entry point at '{vitepressEntry}' does not exist. Run `npm ci` under {docsRoot} first (the OneTimeSetUp does this when node_modules is missing)."); + } + + var psi = new ProcessStartInfo + { + FileName = "node", + WorkingDirectory = docsRoot, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + // Mirror package.json's `build` script memory budget — vitepress + // build's Vue SSR pass can exceed V8's default 2 GB old-space + // when the source tree is thousands of pages. + psi.ArgumentList.Add("--max-old-space-size=8192"); + psi.ArgumentList.Add(vitepressEntry); + psi.ArgumentList.Add("build"); + + var proc = Process.Start(psi) + ?? throw new InvalidOperationException("Failed to start `node … vitepress build` process"); + + var stdoutTask = proc.StandardOutput.ReadToEndAsync(); + var stderrTask = proc.StandardError.ReadToEndAsync(); + Task.WaitAll(stdoutTask, stderrTask); + var stdout = stdoutTask.Result; + var stderr = stderrTask.Result; + proc.WaitForExit(); + + if (proc.ExitCode != 0) + { + throw new InvalidOperationException( + $"`node … vitepress.js build` exited {proc.ExitCode}{Environment.NewLine}stdout:{Environment.NewLine}{stdout}{Environment.NewLine}stderr:{Environment.NewLine}{stderr}"); + } + } + private static void RunNpm(string arguments, string workingDirectory) { var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); From 9374628635861367add817eb79ba2dff522b8d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 19 Aug 2026 19:45:13 +0200 Subject: [PATCH 3/7] fix(docs-tests): bound vitepress build + dispose Process + sync docs Convergent MEDIUM findings from the PR #227 6-agent Ultrareview (F-SEC-001, F-SEC-002, F-IMP-001, F-CR-001) around the new RunVitepressBuild helper and MEDIUM/HIGH stale-doc-reference findings (F-DOC-001..006) around the producer/consumer mode shift. Deferred MEDIUM items (node preflight, dist-lock, bounded stdout, RunProcess helper, predicate/failure-path coverage) tracked in #238 with concrete sketches so each lands as its own scoped PR. RunVitepressBuild fixture-side: * VitepressBuildTimeoutMs = 20 min bounds the child; on expiry the process tree is killed, drained partials captured, and an InvalidOperationException surfaces the diagnostic rather than a wall-clock CI timeout that discards it. * "using" on the Process handle guarantees OS handle + pipe release on every path (previously leaked on drain/wait/exit-code throws; warm CI runners accumulated handles across reruns). * XML doc block updated to enumerate the timeout arm alongside the pre-existing missing-entry and non-zero-exit arms. Stale doc sync (atomic bug-class fix): * tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs class summary, ServerReadyTimeoutMs summary, and OneTimeSetUp_Rebuilds_Dist remarks: replace "npm ci + npm run build" and the sentinel-only invariant with the new producer-always-rebuild + consumer-shard contract and the direct-vitepress-invocation rationale. * docs/development/docs-site.md: rewrite the end-to-end route-check section (lines 64, 72) to explain producer vs. consumer mode and why the fixture bypasses npm run build. * .github/workflows/dotnet.yml: expand the docs-prepare header comment to document the fixture's two-mode contract so the next maintainer editing the workflow does not re-encode the old sentinel-only invariant. Verification (bluefin, verify/pr227 worktree): * dotnet build MTConnect.NET.sln -p:IntegrationCoverage=true -> 0 warnings, 0 errors, 5s. * dotnet test tests/MTConnect.NET-Docs-Tests --no-build -> 72/72 passed, 57s. * dotnet test MTConnect.NET.sln --no-build -> 5,033/5,033 passed across every test project. * node --max-old-space-size=8192 node_modules/vitepress/bin/vitepress.js build -> build complete in 20.41s. Refs: TrakHound/MTConnect.NET#238 --- .github/workflows/dotnet.yml | 15 ++- docs/development/docs-site.md | 4 +- .../RouteCheckTests.cs | 92 +++++++++++++++---- 3 files changed, 85 insertions(+), 26 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 8793ee637..ebbf9b3f0 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -328,10 +328,17 @@ jobs: # ref.sh` → `docfx metadata`). The built dist tree is uploaded as a # workflow artifact so the sharded route-check job below can skip # the npm + docfx + build wall-clock (~5 min) entirely. The test - # fixture's [OneTimeSetUp] checks for docs/.vitepress/dist/index.html - # and skips the `npm ci && npm run build` bootstrap when the file is - # present, so a shard that downloads the artifact into the right - # path bypasses the bootstrap altogether. + # fixture's [OneTimeSetUp] contract has two modes: + # - Consumer mode (ROUTE_SHARD_TOTAL > 1, i.e. the sharded matrix + # leg below): checks for docs/.vitepress/dist/index.html and + # skips the vitepress build bootstrap when the sentinel is + # present. A shard that downloads this job's artifact into the + # right path bypasses the bootstrap altogether. + # - Producer mode (ROUTE_SHARD_TOTAL <= 1, i.e. local + unsharded + # CI): always rebuilds by invoking `vitepress build` directly + # (bypassing the `prebuild` docfx hook so the shard runners' + # missing docfx is a non-issue) so a warm-cache developer run + # still walks a dist tree generated from the CURRENT source. # ------------------------------------------------------------------ docs-prepare: name: docs-prepare diff --git a/docs/development/docs-site.md b/docs/development/docs-site.md index bc88dc565..61801b7af 100644 --- a/docs/development/docs-site.md +++ b/docs/development/docs-site.md @@ -61,7 +61,7 @@ The classic symptom of a base mismatch is a deployed page that renders as raw HT ## End-to-end route check -`tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs` is a Playwright e2e fixture that builds the docs site, spawns `vitepress preview` against the built `dist/` tree, walks every route the markdown source tree implies in a headless Chromium browser, and asserts no client-side 404s. CI runs it on the `ubuntu-latest` matrix leg of `.github/workflows/dotnet.yml` (the `windows-latest` leg filters `Category=E2E` out — hosted Windows runners do not carry Linux-image Docker, and the test fixture's `npm ci && npm run build` bootstrap is the easier target to keep Linux-only). +`tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs` is a Playwright e2e fixture that builds the docs site, spawns `vitepress preview` against the built `dist/` tree, walks every route the markdown source tree implies in a headless Chromium browser, and asserts no client-side 404s. CI runs it on the `ubuntu-latest` matrix leg of `.github/workflows/dotnet.yml` (the `windows-latest` leg filters `Category=E2E` out — hosted Windows runners do not carry Linux-image Docker, and the test fixture's `npm ci` + direct `vitepress build` bootstrap is the easier target to keep Linux-only). Run locally from the repo root: @@ -69,7 +69,7 @@ Run locally from the repo root: dotnet test tests/MTConnect.NET-Docs-Tests --filter Category=E2E ``` -On the first run the fixture installs the chromium binary the Playwright .NET binding drives (~150 MB; cached on subsequent runs) and — if `docs/.vitepress/dist/` is missing — invokes `npm ci && npm run build` from `docs/` to produce a preview-able site. Subsequent runs reuse both, so a warm working tree completes in a couple of minutes; a cold checkout takes longer because the build artefact is rebuilt from scratch. +On the first run the fixture installs the chromium binary the Playwright .NET binding drives (~150 MB; cached on subsequent runs) and, in producer mode (local + unsharded CI, i.e. `ROUTE_SHARD_TOTAL <= 1`), invokes `npm ci` when `docs/node_modules/` is missing and then always invokes `vitepress build` directly from `docs/` — bypassing the `package.json` `prebuild` hook (`docs/scripts/generate-api-ref.sh` → `docfx metadata`) that would otherwise clobber every touched project's `obj/project.assets.json` back to a Debug-only `net8.0` view and race any in-flight multi-TFM Release build. In consumer mode (sharded CI matrix with `ROUTE_SHARD_TOTAL > 1`), the shard downloads a `dist/` tree from the `docs-prepare` workflow artifact and honours the `docs/.vitepress/dist/index.html` sentinel, skipping the rebuild. Subsequent local runs reuse the cached `node_modules/` and Playwright chromium, so a warm producer-mode run completes in a couple of minutes; a cold checkout takes longer because the build artefact is rebuilt from scratch. Failure output names every route that surfaced as a 404 along with which of the two signals fired—the `.NotFound` element rendered by the VitePress default theme's NotFound component, or `document.title` starting with `404` (the static `404.html` emits `404 | MTConnect.NET`, so a prefix match catches it regardless of the trailing site-title suffix). Typical fixes: diff --git a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs index b5bc927d3..3f95ad36e 100644 --- a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs +++ b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs @@ -31,8 +31,13 @@ namespace MTConnect.NET_Docs_Tests; /// dotnet test tests/MTConnect.NET-Docs-Tests --filter Category=E2E /// /// Prerequisites: -/// - Node.js (the setup invokes `npm ci` + `npm run build` if the -/// docs/.vitepress/dist/ artifact is missing). +/// - Node.js. In producer mode (local + unsharded CI) the setup +/// runs `npm ci` when `node_modules/` is absent and then always +/// invokes `vitepress build` directly (bypassing the docfx +/// `prebuild` hook that would clobber `obj/project.assets.json`). +/// In consumer mode (`ROUTE_SHARD_TOTAL > 1`) the shard consumes +/// a `dist/` produced by the `docs-prepare` CI job and skips the +/// rebuild when `docs/.vitepress/dist/index.html` is present. /// - The Microsoft.Playwright package's chromium browser binary /// (installed automatically by the fixture's one-time setup). /// @@ -54,13 +59,23 @@ public class RouteCheckTests private const int ServerReadyPollMs = 200; /// Hard deadline for the preview-server bind. 60 s - /// accommodates a cold CI runner where `npm ci` + `npm run build` + /// accommodates a cold CI runner where `npm ci` + `vitepress build` /// + vitepress startup land before the first port probe — anything /// past that is a real failure (dist/ missing, port collision, /// vitepress CLI usage error) worth surfacing as a TimeoutException /// with the drained startup log. private const int ServerReadyTimeoutMs = 60_000; + /// Hard deadline for the vitepress build spawned by + /// . 20 minutes bounds the worst + /// documented cold path (cold node_modules cache + full SSR pass + /// on a slow runner completes in ~5 min); anything past that + /// implies a hang (deadlocked worker, HMR loop, wedged fetch) + /// worth surfacing as an InvalidOperationException with the + /// drained output rather than a wall-clock CI timeout that + /// discards the diagnostic. + private const int VitepressBuildTimeoutMs = 20 * 60 * 1000; + /// Per-page navigation timeout. 30 s covers a slow runner /// with a cold network cache; anything past that is a real failure /// (vitepress hang, JS exception that prevents Load) worth failing @@ -144,7 +159,7 @@ public async Task OneTimeSetUp() // // Consumer mode (sharded CI matrix, ROUTE_SHARD_TOTAL > 1): // CI's sharded route-check jobs download the dist/ tree - // from the `docs-prepare` workflow artefact and skip the + // from the `docs-prepare` workflow artifact and skip the // rebuild. The docs-prepare job is the single docfx-owning // producer. // @@ -500,15 +515,15 @@ public async Task Landing_Hero_Image_Asset_Resolves() /// /// /// Sharded CI runs (matrix env var ROUTE_SHARD_TOTAL > 1) - /// download the dist artefact from the upstream docs-prepare - /// job and intentionally bypass the in-fixture build — the shard - /// runners do not install docfx, so re-running npm run build - /// would fail on the prebuild hook - /// (scripts/generate-api-ref.shdocfx metadata). In - /// that mode the upstream job is the producer and this fixture is a - /// pure consumer, so the mtime invariant does not apply and the test - /// is inconclusive. Local invocations and the unsharded leg still - /// enforce it. + /// download the dist artifact from the upstream docs-prepare + /// job and intentionally bypass the in-fixture build — the + /// docs-prepare job is the single docfx-owning producer + /// and each shard is a pure consumer, so the mtime invariant does + /// not apply and the test is inconclusive. Local invocations and + /// the unsharded CI leg still enforce it (the producer path + /// invokes vitepress build directly, bypassing the + /// package.json prebuild hook so the shard runners' + /// missing docfx binary is a non-issue for the fixture itself). /// [Test] [Category("E2E")] @@ -835,8 +850,13 @@ private static void StopPreviewServer(Process? proc) /// net9.0, net10.0, …). This helper resolves /// node_modules/vitepress/bin/vitepress.js relative to the /// docs root, drains stdout+stderr concurrently to avoid the - /// classic pipe-deadlock pattern, and rethrows with the captured - /// output when the child exits non-zero. + /// classic pipe-deadlock pattern, bounds the child by + /// so a wedged worker + /// surfaces as an actionable exception rather than a job-level + /// timeout that discards the diagnostic, and rethrows with the + /// captured output when the child exits non-zero. The + /// handle is disposed on every path so a + /// warm test-runner does not leak file descriptors across reruns. /// /// /// Absolute path to the docs site (docs/ under the repo @@ -846,9 +866,12 @@ private static void StopPreviewServer(Process? proc) /// /// Thrown when the vitepress binary cannot be located, /// returns - /// , or the child process exits with a - /// non-zero code (the captured stdout and stderr are appended to - /// the exception message for diagnosis). + /// , the child fails to exit within + /// milliseconds (the child + /// tree is killed before the exception is thrown), or the child + /// process exits with a non-zero code. The captured stdout and + /// stderr are appended to the exception message in every failure + /// mode for diagnosis. /// private static void RunVitepressBuild(string docsRoot) { @@ -875,15 +898,44 @@ private static void RunVitepressBuild(string docsRoot) psi.ArgumentList.Add(vitepressEntry); psi.ArgumentList.Add("build"); - var proc = Process.Start(psi) + // `using` on Process guarantees the OS handle + redirected + // pipes are released even when the drain/wait/exit-code path + // throws — a warm test-runner otherwise accumulates handles + // and can starve pipes across reruns. + using var proc = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start `node … vitepress build` process"); var stdoutTask = proc.StandardOutput.ReadToEndAsync(); var stderrTask = proc.StandardError.ReadToEndAsync(); + + // Bound the child wait so a wedged vitepress (deadlocked + // Vue-SSR worker, hung fetch, infinite HMR loop) surfaces as + // an actionable exception with the drained partial output + // rather than a wall-clock CI timeout that discards it. + if (!proc.WaitForExit(VitepressBuildTimeoutMs)) + { + try + { + proc.Kill(entireProcessTree: true); + } + catch + { + // Best-effort — the child may already be exiting; a + // failure to signal is not itself the diagnostic. + } + // Give the drains one last chance to complete after the + // kill; ignore any fault so the timeout message is what + // the caller sees. + try { Task.WaitAll(new[] { stdoutTask, stderrTask }, millisecondsTimeout: 2_000); } catch { } + var partialStdout = stdoutTask.IsCompletedSuccessfully ? stdoutTask.Result : ""; + var partialStderr = stderrTask.IsCompletedSuccessfully ? stderrTask.Result : ""; + throw new InvalidOperationException( + $"`node … vitepress.js build` did not exit within {VitepressBuildTimeoutMs} ms — killed the child tree and captured what stdout/stderr had been drained.{Environment.NewLine}stdout:{Environment.NewLine}{partialStdout}{Environment.NewLine}stderr:{Environment.NewLine}{partialStderr}"); + } + Task.WaitAll(stdoutTask, stderrTask); var stdout = stdoutTask.Result; var stderr = stderrTask.Result; - proc.WaitForExit(); if (proc.ExitCode != 0) { From 93ef439262265469c427fde9a4795069076d043b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Tue, 25 Aug 2026 08:35:40 +0200 Subject: [PATCH 4/7] fix(docs-tests): harden RunNpm to match RunVitepressBuild, sanitize diagnostics Ultrareview cycle-1 convergent finding (code-review F-CR-001, simplification F-SIMP-001, improvement F-IMP-001): RunNpm leaked its Process handle and had no timeout bound, the exact defect class this PR's own fix commit resolved in RunVitepressBuild. Extract both into a shared RunProcess helper so `npm ci` gets the same dispose-on-every-path + bounded-wait-with-kill hardening. Also: sanitize control characters out of captured child-process output before it lands in exception messages (security-audit F-SEC-001, LOW); correct the ServerReadyTimeoutMs doc comment, which claimed the 60 s bind deadline covers npm/build time that has, in fact, already completed by the time the preview server starts (code-review F-CR-002); cross-reference the shared prebuild-hook obj/ race from EnvironmentVariables_Page_Is_In_Sync_With_Source back to RunVitepressBuild's remarks (improvement F-IMP-002). 72/72 MTConnect.NET-Docs-Tests pass; build 0/0; format clean. --- .../DocsReferenceGenerationTests.cs | 5 +- .../RouteCheckTests.cs | 158 +++++++++++------- 2 files changed, 102 insertions(+), 61 deletions(-) diff --git a/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs b/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs index 922a42391..714da6285 100644 --- a/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs +++ b/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs @@ -64,7 +64,10 @@ public void HttpApi_Page_Is_In_Sync_With_Source() } } - /// Pins the behaviour expressed by the test name: environment variables page is in sync with source. + /// Pins the behaviour expressed by the test name: environment variables page is in sync with source. + /// Historically flaky as collateral damage from a prebuild-hook obj/ race triggered elsewhere in the + /// same dotnet test invocation — see 's remarks on why + /// producer-mode OneTimeSetUp no longer shells out through `npm run build`'s `prebuild` hook. [Test] public void EnvironmentVariables_Page_Is_In_Sync_With_Source() { diff --git a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs index 3f95ad36e..a1365f8e1 100644 --- a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs +++ b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs @@ -58,12 +58,14 @@ public class RouteCheckTests /// cost trivial while still ringing the door bell ~5x per second. private const int ServerReadyPollMs = 200; - /// Hard deadline for the preview-server bind. 60 s - /// accommodates a cold CI runner where `npm ci` + `vitepress build` - /// + vitepress startup land before the first port probe — anything - /// past that is a real failure (dist/ missing, port collision, - /// vitepress CLI usage error) worth surfacing as a TimeoutException - /// with the drained startup log. + /// Hard deadline for the preview-server bind. `npm ci` and + /// `vitepress build` are earlier, sequential OneTimeSetUp + /// stages that already ran to completion before this countdown + /// starts (see ), so 60 s covers only + /// vitepress preview's own startup — anything past that is a real + /// failure (dist/ missing, port collision, vitepress CLI usage + /// error) worth surfacing as a TimeoutException with the drained + /// startup log. private const int ServerReadyTimeoutMs = 60_000; /// Hard deadline for the vitepress build spawned by @@ -76,6 +78,15 @@ public class RouteCheckTests /// discards the diagnostic. private const int VitepressBuildTimeoutMs = 20 * 60 * 1000; + /// Hard deadline for `npm ci`, spawned by + /// when docs/node_modules is absent. 10 + /// minutes bounds a cold registry fetch on a slow runner; anything + /// past that implies a hang (registry outage, interactive prompt, + /// corrupt lockfile) worth surfacing as an InvalidOperationException + /// with the drained output rather than hanging OneTimeSetUp + /// indefinitely with no diagnostic. + private const int NpmTimeoutMs = 10 * 60 * 1000; + /// Per-page navigation timeout. 30 s covers a slow runner /// with a cold network cache; anything past that is a real failure /// (vitepress hang, JS exception that prevents Load) worth failing @@ -898,21 +909,73 @@ private static void RunVitepressBuild(string docsRoot) psi.ArgumentList.Add(vitepressEntry); psi.ArgumentList.Add("build"); - // `using` on Process guarantees the OS handle + redirected - // pipes are released even when the drain/wait/exit-code path - // throws — a warm test-runner otherwise accumulates handles - // and can starve pipes across reruns. + RunProcess(psi, VitepressBuildTimeoutMs, "`node … vitepress.js build`"); + } + + private static void RunNpm(string arguments, string workingDirectory) + { + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + var fileName = isWindows ? "npm.cmd" : "npm"; + + var psi = new ProcessStartInfo + { + FileName = fileName, + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + foreach (var token in arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + psi.ArgumentList.Add(token); + } + + RunProcess(psi, NpmTimeoutMs, $"`npm {arguments}`"); + } + + /// + /// Spawn , drain stdout/stderr concurrently to + /// avoid the classic pipe-deadlock pattern, bound the wait by + /// so a wedged child surfaces as an + /// actionable exception rather than a wall-clock CI timeout that + /// discards the diagnostic, and rethrow with the captured output on + /// either a timeout (after killing the child tree) or a non-zero + /// exit code. Shared by and + /// so both child-process launches get the same + /// dispose/timeout/kill hardening rather than diverging over time. + /// + /// Fully configured start info; stdout/stderr must + /// already be redirected. + /// Hard wait deadline in milliseconds. + /// Human-readable command label used in + /// exception messages (e.g. `npm ci`). + /// + /// Thrown when returns + /// , the child fails to exit within + /// milliseconds (the child tree is + /// killed before the exception is thrown), or the child process + /// exits with a non-zero code. The captured stdout and stderr are + /// appended to the exception message in every failure mode for + /// diagnosis. + /// + private static void RunProcess(ProcessStartInfo psi, int timeoutMs, string label) + { + // `using` guarantees the OS handle + redirected pipes are + // released even when the drain/wait/exit-code path throws — a + // warm test-runner otherwise accumulates handles and can starve + // pipes across reruns. using var proc = Process.Start(psi) - ?? throw new InvalidOperationException("Failed to start `node … vitepress build` process"); + ?? throw new InvalidOperationException($"Failed to start {label}"); var stdoutTask = proc.StandardOutput.ReadToEndAsync(); var stderrTask = proc.StandardError.ReadToEndAsync(); - // Bound the child wait so a wedged vitepress (deadlocked - // Vue-SSR worker, hung fetch, infinite HMR loop) surfaces as - // an actionable exception with the drained partial output - // rather than a wall-clock CI timeout that discards it. - if (!proc.WaitForExit(VitepressBuildTimeoutMs)) + // Bound the child wait so a wedged process (deadlocked worker, + // hung fetch, interactive prompt, infinite loop) surfaces as an + // actionable exception with the drained partial output rather + // than a wall-clock CI timeout that discards it. + if (!proc.WaitForExit(timeoutMs)) { try { @@ -927,61 +990,36 @@ private static void RunVitepressBuild(string docsRoot) // kill; ignore any fault so the timeout message is what // the caller sees. try { Task.WaitAll(new[] { stdoutTask, stderrTask }, millisecondsTimeout: 2_000); } catch { } - var partialStdout = stdoutTask.IsCompletedSuccessfully ? stdoutTask.Result : ""; - var partialStderr = stderrTask.IsCompletedSuccessfully ? stderrTask.Result : ""; + var partialStdout = stdoutTask.IsCompletedSuccessfully ? SanitizeForException(stdoutTask.Result) : ""; + var partialStderr = stderrTask.IsCompletedSuccessfully ? SanitizeForException(stderrTask.Result) : ""; throw new InvalidOperationException( - $"`node … vitepress.js build` did not exit within {VitepressBuildTimeoutMs} ms — killed the child tree and captured what stdout/stderr had been drained.{Environment.NewLine}stdout:{Environment.NewLine}{partialStdout}{Environment.NewLine}stderr:{Environment.NewLine}{partialStderr}"); + $"{label} did not exit within {timeoutMs} ms — killed the child tree and captured what stdout/stderr had been drained.{Environment.NewLine}stdout:{Environment.NewLine}{partialStdout}{Environment.NewLine}stderr:{Environment.NewLine}{partialStderr}"); } Task.WaitAll(stdoutTask, stderrTask); - var stdout = stdoutTask.Result; - var stderr = stderrTask.Result; + var stdout = SanitizeForException(stdoutTask.Result); + var stderr = SanitizeForException(stderrTask.Result); if (proc.ExitCode != 0) { throw new InvalidOperationException( - $"`node … vitepress.js build` exited {proc.ExitCode}{Environment.NewLine}stdout:{Environment.NewLine}{stdout}{Environment.NewLine}stderr:{Environment.NewLine}{stderr}"); + $"{label} exited {proc.ExitCode}{Environment.NewLine}stdout:{Environment.NewLine}{stdout}{Environment.NewLine}stderr:{Environment.NewLine}{stderr}"); } } - private static void RunNpm(string arguments, string workingDirectory) + /// + /// Strip ASCII control characters (other than newline/carriage + /// return/tab) from captured child-process output before it is + /// interpolated into an exception message. A compromised or + /// misbehaving npm dependency could otherwise emit control + /// sequences that spoof adjacent lines in aggregated CI log + /// viewers; this keeps the diagnostic text inert. + /// + /// Raw captured stdout or stderr. + /// with disallowed control + /// characters removed. + private static string SanitizeForException(string text) { - var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - var fileName = isWindows ? "npm.cmd" : "npm"; - - var psi = new ProcessStartInfo - { - FileName = fileName, - WorkingDirectory = workingDirectory, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }; - foreach (var token in arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries)) - { - psi.ArgumentList.Add(token); - } - - var proc = Process.Start(psi) - ?? throw new InvalidOperationException($"Failed to start `npm {arguments}`"); - - // Drain both pipes concurrently so the child doesn't block on a - // full stderr buffer while the parent waits on stdout — the - // classic pipe-deadlock pattern. `npm ci` emits stderr volume in - // the form of deprecation warnings and peer-dep notices that on a - // verbose run can exceed the OS pipe buffer (typically 64 KB). - var stdoutTask = proc.StandardOutput.ReadToEndAsync(); - var stderrTask = proc.StandardError.ReadToEndAsync(); - Task.WaitAll(stdoutTask, stderrTask); - var stdout = stdoutTask.Result; - var stderr = stderrTask.Result; - proc.WaitForExit(); - - if (proc.ExitCode != 0) - { - throw new InvalidOperationException( - $"`npm {arguments}` exited {proc.ExitCode}{Environment.NewLine}stdout:{Environment.NewLine}{stdout}{Environment.NewLine}stderr:{Environment.NewLine}{stderr}"); - } + return new string(text.Where(c => !char.IsControl(c) || c is '\n' or '\r' or '\t').ToArray()); } } From 6be7f078bac3a26dfdd72d2ee5fb3bd144834fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Tue, 25 Aug 2026 08:44:28 +0200 Subject: [PATCH 5/7] fix(docs-gen): exclude .claude from EnvVarInventory's repo-root walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bluefin's full-solution `dotnet test MTConnect.NET.sln` run on integration/up-to-pr-227 surfaced EnvironmentVariables_Page_Is_In_Sync_With_Source as flaky (fails at solution scope, passes when the Docs-Tests project runs alone). Root cause: EnvVarInventory.Collect walks repoRoot recursively for *.cs files and only excludes bin/obj/node_modules/.git/ .vitepress by directory name — it does not exclude .claude/worktrees/, the repo's convention for nested agent-session git worktrees. On a host with sibling worktrees checked out under .claude/worktrees/ (as bluefin had at test time), the scan picks up GetEnvironmentVariable call sites from whatever branch each nested worktree happens to have checked out, producing a false "out of sync" verdict unrelated to the current branch's actual source tree. Excludes .claude the same way bin/obj/.git already are. Verified locally (72/72 Docs-Tests pass, this worktree has no nested .claude pollution so this is a defensive/root-cause fix pending bluefin re-verification against the actual polluted host state). --- build/MTConnect.NET-DocsGen/EnvVarInventory.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build/MTConnect.NET-DocsGen/EnvVarInventory.cs b/build/MTConnect.NET-DocsGen/EnvVarInventory.cs index 9feda8adc..cd32b93d6 100644 --- a/build/MTConnect.NET-DocsGen/EnvVarInventory.cs +++ b/build/MTConnect.NET-DocsGen/EnvVarInventory.cs @@ -45,6 +45,15 @@ public static class EnvVarInventory private static readonly string[] ExcludedDirectoryNames = { "bin", "obj", "node_modules", ".git", ".vitepress", + // ".claude" holds sibling git-worktree checkouts (see repo + // convention: .claude/worktrees//) used by agent sessions + // for isolated branch work. Left unexcluded, a repo-root-relative + // walk descends into every nested worktree and mixes in + // env-var reads from whatever branch each one happens to have + // checked out — producing a false "out of sync" verdict against + // docs/reference/environment-variables.md that has nothing to + // do with the actual source tree on the current branch. + ".claude", }; // Match `MTCONNECT_FOO`, `DOTNET_BAR`, plus any other ALL-CAPS env-var From 3ad6862a15fa54ba06cfb6551fcb1e54b11eace3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Wed, 26 Aug 2026 23:35:45 +0200 Subject: [PATCH 6/7] =?UTF-8?q?fix(docs-pipeline):=20rewrite=20docfx=20met?= =?UTF-8?q?adata=20src=20DLLs=20=E2=86=92=20csproj=20to=20resolve=20CS0518?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docs-prepare CI job and local `bash docs/scripts/generate-api-ref.sh` runs failed with 120 CS0518 errors (`Predefined type 'System.Object' / 'System.String' / 'System.Void' is not defined or imported`) plus 3,943 `InvalidAssemblyReference` warnings when docfx metadata loaded the 43 pre-built Debug/net8.0 DLLs listed in `docs/.docfx/docfx.json` `metadata[].src.files`. Root cause: docfx's DLL-loader path cannot resolve corelib references (System.Runtime / mscorlib) because the DLL tree carries no matching reference assemblies alongside; docfx then falls back to synthesising a stub compilation that has no `System.*` imports, which trips CS0518 on every emitted AssemblyInfo.cs. Fix — switch the metadata source from DLLs to `.csproj` project files so docfx uses MSBuild workspace loading. MSBuild resolves references via the project's `` and the .NET SDK's reference- assembly packs, exactly as `dotnet build` does. This is the intended docfx analysis path for library documentation; the DLL path is only meant for third-party assemblies without source. Composite of the two sub-fixes both required for a Linux-CI-runnable metadata run: 1. Metadata src `files:` now lists the 43 `.csproj` paths that mirror the previous DLL list one-for-one (including the `templates/mtconnect.net-agent/content/MTConnect.NET-Embedded- Agent/Agent.csproj` naming override). 2. `metadata[].properties`: `{ TargetFramework: "net8.0", Configuration: "Debug" }` pins the msbuild workspace to the single modern TFM. Without this, multi-TFM projects (all libraries + agent + adapter + examples target net461/462/47/47.1/47.2/48/8.0/9.0) trigger a per-TFM msbuild load attempt; the .NET Framework 4.x TFMs then fail on Linux with "reference assemblies for .NETFramework,Version=v4.6.1 were not found" because the developer packs are Windows-only. Pinning to net8.0 keeps the CI docs-prepare job (which installs only 8.0.x + 9.0.x SDKs) buildable and mirrors the analysis target the docfx metadata run was always intended for. Verified on bluefin against `docfx 2.78.5` + `.NET 10.0.302`: `bash docs/scripts/generate-api-ref.sh` now completes cleanly with 0 errors, 2 pre-existing DeviceFinder XML-doc warnings (unchanged), and 2,242 markdown pages generated. Fix is master-side pre-existing bug — not scoped originally to this PR, but the docs-prepare CI job was also red on this branch's own head and every downstream cascade branch, so folding the fix in here unblocks the whole train. --- docs/.docfx/docfx.json | 94 ++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/docs/.docfx/docfx.json b/docs/.docfx/docfx.json index c196606e9..362abff3f 100644 --- a/docs/.docfx/docfx.json +++ b/docs/.docfx/docfx.json @@ -4,50 +4,50 @@ "src": [ { "files": [ - "libraries/MTConnect.NET-Common/bin/Debug/net8.0/MTConnect.NET-Common.dll", - "libraries/MTConnect.NET-DeviceFinder/bin/Debug/net8.0/MTConnect.NET-DeviceFinder.dll", - "libraries/MTConnect.NET-HTTP/bin/Debug/net8.0/MTConnect.NET-HTTP.dll", - "libraries/MTConnect.NET-JSON/bin/Debug/net8.0/MTConnect.NET-JSON.dll", - "libraries/MTConnect.NET-JSON-cppagent/bin/Debug/net8.0/MTConnect.NET-JSON-cppagent.dll", - "libraries/MTConnect.NET-MQTT/bin/Debug/net8.0/MTConnect.NET-MQTT.dll", - "libraries/MTConnect.NET-Protobuf/bin/Debug/net8.0/MTConnect.NET-Protobuf.dll", - "libraries/MTConnect.NET-Services/bin/Debug/net8.0/MTConnect.NET-Services.dll", - "libraries/MTConnect.NET-SHDR/bin/Debug/net8.0/MTConnect.NET-SHDR.dll", - "libraries/MTConnect.NET-SysML/bin/Debug/net8.0/MTConnect.NET-SysML.dll", - "libraries/MTConnect.NET-TLS/bin/Debug/net8.0/MTConnect.NET-TLS.dll", - "libraries/MTConnect.NET-XML/bin/Debug/net8.0/MTConnect.NET-XML.dll", - "libraries/MTConnect.NET/bin/Debug/net8.0/MTConnect.NET.dll", - "agent/MTConnect.NET-Agent/bin/Debug/net8.0/MTConnect.NET-Agent.dll", - "agent/MTConnect.NET-Applications-Agents/bin/Debug/net8.0/MTConnect.NET-Applications-Agents.dll", - "agent/Modules/MTConnect.NET-AgentModule-HttpServer/bin/Debug/net8.0/MTConnect.NET-AgentModule-HttpServer.dll", - "agent/Modules/MTConnect.NET-AgentModule-HttpAdapter/bin/Debug/net8.0/MTConnect.NET-AgentModule-HttpAdapter.dll", - "agent/Modules/MTConnect.NET-AgentModule-MqttAdapter/bin/Debug/net8.0/MTConnect.NET-AgentModule-MqttAdapter.dll", - "agent/Modules/MTConnect.NET-AgentModule-MqttBroker/bin/Debug/net8.0/MTConnect.NET-AgentModule-MqttBroker.dll", - "agent/Modules/MTConnect.NET-AgentModule-MqttRelay/bin/Debug/net8.0/MTConnect.NET-AgentModule-MqttRelay.dll", - "agent/Modules/MTConnect.NET-AgentModule-ShdrAdapter/bin/Debug/net8.0/MTConnect.NET-AgentModule-ShdrAdapter.dll", - "agent/Processors/MTConnect.NET-AgentProcessor-Python/bin/Debug/net8.0/MTConnect.NET-AgentProcessor-Python.dll", - "adapter/MTConnect.NET-Adapter/bin/Debug/net8.0/MTConnect.NET-Adapter.dll", - "adapter/MTConnect.NET-Applications-Adapter/bin/Debug/net8.0/MTConnect.NET-Applications-Adapter.dll", - "adapter/Modules/MTConnect.NET-AdapterModule-MQTT/bin/Debug/net8.0/MTConnect.NET-AdapterModule-MQTT.dll", - "adapter/Modules/MTConnect.NET-AdapterModule-SHDR/bin/Debug/net8.0/MTConnect.NET-AdapterModule-SHDR.dll", - "build/MTConnect.NET-SysML-Import/bin/Debug/net8.0/MTConnect.NET-SysML-Import.dll", - "build/MTConnect.NET.Builder/bin/Debug/net8.0/MTConnect.NET.Builder.dll", - "examples/MTConnect.NET-Agent-Embedded/bin/Debug/net8.0/MTConnect.NET-Agent-Embedded.dll", - "examples/MTConnect.NET-Client-HTTP/bin/Debug/net8.0/MTConnect.NET-Client-HTTP.dll", - "examples/MTConnect.NET-Client-MQTT/bin/Debug/net8.0/MTConnect.NET-Client-MQTT.dll", - "examples/MTConnect.NET-Client-SHDR/bin/Debug/net8.0/MTConnect.NET-Client-SHDR.dll", - "templates/mtconnect.net-agent/content/MTConnect.NET-Embedded-Agent/bin/Debug/net8.0/agent.dll", - "tests/Compliance/MTConnect-Compliance-Tests/bin/Debug/net8.0/MTConnect-Compliance-Tests.dll", - "tests/MTConnect.NET-AgentModule-MqttRelay-Tests/bin/Debug/net8.0/MTConnect.NET-AgentModule-MqttRelay-Tests.dll", - "tests/MTConnect.NET-Common-Tests/bin/Debug/net8.0/MTConnect.NET-Common-Tests.dll", - "tests/MTConnect.NET-Docs-Tests/bin/Debug/net8.0/MTConnect.NET-Docs-Tests.dll", - "tests/MTConnect.NET-HTTP-Tests/bin/Debug/net8.0/MTConnect.NET-HTTP-Tests.dll", - "tests/MTConnect.NET-Integration-Tests/bin/Debug/net8.0/MTConnect.NET-Integration-Tests.dll", - "tests/MTConnect.NET-JSON-cppagent-Tests/bin/Debug/net8.0/MTConnect.NET-JSON-cppagent-Tests.dll", - "tests/MTConnect.NET-JSON-Tests/bin/Debug/net8.0/MTConnect.NET-JSON-Tests.dll", - "tests/MTConnect.NET-SHDR-Tests/bin/Debug/net8.0/MTConnect.NET-SHDR-Tests.dll", - "tests/MTConnect.NET-Tests-Agents/bin/Debug/net8.0/MTConnect.NET-Tests-Agents.dll", - "tests/MTConnect.NET-XML-Tests/bin/Debug/net8.0/MTConnect.NET-XML-Tests.dll" + "libraries/MTConnect.NET-Common/MTConnect.NET-Common.csproj", + "libraries/MTConnect.NET-DeviceFinder/MTConnect.NET-DeviceFinder.csproj", + "libraries/MTConnect.NET-HTTP/MTConnect.NET-HTTP.csproj", + "libraries/MTConnect.NET-JSON/MTConnect.NET-JSON.csproj", + "libraries/MTConnect.NET-JSON-cppagent/MTConnect.NET-JSON-cppagent.csproj", + "libraries/MTConnect.NET-MQTT/MTConnect.NET-MQTT.csproj", + "libraries/MTConnect.NET-Protobuf/MTConnect.NET-Protobuf.csproj", + "libraries/MTConnect.NET-Services/MTConnect.NET-Services.csproj", + "libraries/MTConnect.NET-SHDR/MTConnect.NET-SHDR.csproj", + "libraries/MTConnect.NET-SysML/MTConnect.NET-SysML.csproj", + "libraries/MTConnect.NET-TLS/MTConnect.NET-TLS.csproj", + "libraries/MTConnect.NET-XML/MTConnect.NET-XML.csproj", + "libraries/MTConnect.NET/MTConnect.NET.csproj", + "agent/MTConnect.NET-Agent/MTConnect.NET-Agent.csproj", + "agent/MTConnect.NET-Applications-Agents/MTConnect.NET-Applications-Agents.csproj", + "agent/Modules/MTConnect.NET-AgentModule-HttpServer/MTConnect.NET-AgentModule-HttpServer.csproj", + "agent/Modules/MTConnect.NET-AgentModule-HttpAdapter/MTConnect.NET-AgentModule-HttpAdapter.csproj", + "agent/Modules/MTConnect.NET-AgentModule-MqttAdapter/MTConnect.NET-AgentModule-MqttAdapter.csproj", + "agent/Modules/MTConnect.NET-AgentModule-MqttBroker/MTConnect.NET-AgentModule-MqttBroker.csproj", + "agent/Modules/MTConnect.NET-AgentModule-MqttRelay/MTConnect.NET-AgentModule-MqttRelay.csproj", + "agent/Modules/MTConnect.NET-AgentModule-ShdrAdapter/MTConnect.NET-AgentModule-ShdrAdapter.csproj", + "agent/Processors/MTConnect.NET-AgentProcessor-Python/MTConnect.NET-AgentProcessor-Python.csproj", + "adapter/MTConnect.NET-Adapter/MTConnect.NET-Adapter.csproj", + "adapter/MTConnect.NET-Applications-Adapter/MTConnect.NET-Applications-Adapter.csproj", + "adapter/Modules/MTConnect.NET-AdapterModule-MQTT/MTConnect.NET-AdapterModule-MQTT.csproj", + "adapter/Modules/MTConnect.NET-AdapterModule-SHDR/MTConnect.NET-AdapterModule-SHDR.csproj", + "build/MTConnect.NET-SysML-Import/MTConnect.NET-SysML-Import.csproj", + "build/MTConnect.NET.Builder/MTConnect.NET.Builder.csproj", + "examples/MTConnect.NET-Agent-Embedded/MTConnect.NET-Agent-Embedded.csproj", + "examples/MTConnect.NET-Client-HTTP/MTConnect.NET-Client-HTTP.csproj", + "examples/MTConnect.NET-Client-MQTT/MTConnect.NET-Client-MQTT.csproj", + "examples/MTConnect.NET-Client-SHDR/MTConnect.NET-Client-SHDR.csproj", + "templates/mtconnect.net-agent/content/MTConnect.NET-Embedded-Agent/Agent.csproj", + "tests/Compliance/MTConnect-Compliance-Tests/MTConnect-Compliance-Tests.csproj", + "tests/MTConnect.NET-AgentModule-MqttRelay-Tests/MTConnect.NET-AgentModule-MqttRelay-Tests.csproj", + "tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj", + "tests/MTConnect.NET-Docs-Tests/MTConnect.NET-Docs-Tests.csproj", + "tests/MTConnect.NET-HTTP-Tests/MTConnect.NET-HTTP-Tests.csproj", + "tests/MTConnect.NET-Integration-Tests/MTConnect.NET-Integration-Tests.csproj", + "tests/MTConnect.NET-JSON-cppagent-Tests/MTConnect.NET-JSON-cppagent-Tests.csproj", + "tests/MTConnect.NET-JSON-Tests/MTConnect.NET-JSON-Tests.csproj", + "tests/MTConnect.NET-SHDR-Tests/MTConnect.NET-SHDR-Tests.csproj", + "tests/MTConnect.NET-Tests-Agents/MTConnect.NET-Tests-Agents.csproj", + "tests/MTConnect.NET-XML-Tests/MTConnect.NET-XML-Tests.csproj" ], "src": "../.." } @@ -57,7 +57,11 @@ "namespaceLayout": "flattened", "memberLayout": "samePage", "includePrivateMembers": true, - "filter": "filter.yml" + "filter": "filter.yml", + "properties": { + "TargetFramework": "net8.0", + "Configuration": "Debug" + } } ] } From 5435ed0ff05966f2a4da2e571812a319c416e60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Mon, 31 Aug 2026 09:56:42 +0200 Subject: [PATCH 7/7] docs,tests(docs-tests): swap remaining BrE tokens to AmE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per §1.0d-trigies-vicies-octies (AmE mandatory across every PR-associated surface), three PR-introduced BrE tokens are converted: - docs/development/docs-site.md: 'honours' -> 'honors' in the dist sentinel note - tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs L67: 'behaviour' -> 'behavior' in test docstring - tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs L160: 'Honouring' -> 'Honoring' in policy comment Comment / docstring / prose only; no behavior change. --- docs/development/docs-site.md | 2 +- tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs | 2 +- tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/development/docs-site.md b/docs/development/docs-site.md index 61801b7af..5ab3f3d47 100644 --- a/docs/development/docs-site.md +++ b/docs/development/docs-site.md @@ -69,7 +69,7 @@ Run locally from the repo root: dotnet test tests/MTConnect.NET-Docs-Tests --filter Category=E2E ``` -On the first run the fixture installs the chromium binary the Playwright .NET binding drives (~150 MB; cached on subsequent runs) and, in producer mode (local + unsharded CI, i.e. `ROUTE_SHARD_TOTAL <= 1`), invokes `npm ci` when `docs/node_modules/` is missing and then always invokes `vitepress build` directly from `docs/` — bypassing the `package.json` `prebuild` hook (`docs/scripts/generate-api-ref.sh` → `docfx metadata`) that would otherwise clobber every touched project's `obj/project.assets.json` back to a Debug-only `net8.0` view and race any in-flight multi-TFM Release build. In consumer mode (sharded CI matrix with `ROUTE_SHARD_TOTAL > 1`), the shard downloads a `dist/` tree from the `docs-prepare` workflow artifact and honours the `docs/.vitepress/dist/index.html` sentinel, skipping the rebuild. Subsequent local runs reuse the cached `node_modules/` and Playwright chromium, so a warm producer-mode run completes in a couple of minutes; a cold checkout takes longer because the build artefact is rebuilt from scratch. +On the first run the fixture installs the chromium binary the Playwright .NET binding drives (~150 MB; cached on subsequent runs) and, in producer mode (local + unsharded CI, i.e. `ROUTE_SHARD_TOTAL <= 1`), invokes `npm ci` when `docs/node_modules/` is missing and then always invokes `vitepress build` directly from `docs/` — bypassing the `package.json` `prebuild` hook (`docs/scripts/generate-api-ref.sh` → `docfx metadata`) that would otherwise clobber every touched project's `obj/project.assets.json` back to a Debug-only `net8.0` view and race any in-flight multi-TFM Release build. In consumer mode (sharded CI matrix with `ROUTE_SHARD_TOTAL > 1`), the shard downloads a `dist/` tree from the `docs-prepare` workflow artifact and honors the `docs/.vitepress/dist/index.html` sentinel, skipping the rebuild. Subsequent local runs reuse the cached `node_modules/` and Playwright chromium, so a warm producer-mode run completes in a couple of minutes; a cold checkout takes longer because the build artefact is rebuilt from scratch. Failure output names every route that surfaced as a 404 along with which of the two signals fired—the `.NotFound` element rendered by the VitePress default theme's NotFound component, or `document.title` starting with `404` (the static `404.html` emits `404 | MTConnect.NET`, so a prefix match catches it regardless of the trailing site-title suffix). Typical fixes: diff --git a/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs b/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs index 714da6285..ccb9273d9 100644 --- a/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs +++ b/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs @@ -64,7 +64,7 @@ public void HttpApi_Page_Is_In_Sync_With_Source() } } - /// Pins the behaviour expressed by the test name: environment variables page is in sync with source. + /// Pins the behavior expressed by the test name: environment variables page is in sync with source. /// Historically flaky as collateral damage from a prebuild-hook obj/ race triggered elsewhere in the /// same dotnet test invocation — see 's remarks on why /// producer-mode OneTimeSetUp no longer shells out through `npm run build`'s `prebuild` hook. diff --git a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs index a1365f8e1..b0cd01b7c 100644 --- a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs +++ b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs @@ -157,7 +157,7 @@ public async Task OneTimeSetUp() // fixture is the sole authority on dist/, so it always // rebuilds — a warm-cache local run must still walk a tree // generated from the CURRENT source markdown, current - // config.ts, current sidebar, and so on. Honouring a + // config.ts, current sidebar, and so on. Honoring a // pre-existing dist/index.html sentinel was the earlier // policy and it silently walked a stale tree whenever a // developer re-ran `dotnet test` after editing source: the