Repository navigation
Tests assert the contract, not a snapshot — and a broken skill file no longer vanishes silently - #1225
Conversation
…ing it silently A skill whose YAML front matter cannot be parsed was SKIPPED — correctly, since a throw during mesh startup takes the host down — but skipped SILENTLY: no log, no error, the catalog one entry shorter. The author's only symptom was "my skill doesn't appear and nothing is red". The agent half of content/ai was hardened this way after the 2026-08-07 incident; the skill half never was. Product: SkillMarkdown.TryParse now returns an author-actionable reason, and BuiltInSkillProvider records every file it drops (AiContentLoadFailure) and names it on stderr — the same sink and shape BuiltInAgentProvider already uses. BuiltInAgentProvider gains the matching TryParseEmbeddedNode so its own guard test can assert the file was REPORTED, not merely that null came back. Tests — assert the CONTRACT and the INVARIANTS, not a snapshot of today's data: - The shipped skill set was pinned as a hardcoded 19-element literal duplicated in two files. It is DERIVED FROM content/ai/Skill/*.md, so that was brittle (a legitimate new skill reds CI in N places) and blind (a malformed NEW file leaves the shipped set unchanged, so the literal still matched — green while broken). Replaced by file-derived set equality plus a no-skipped-file guard that fails RED naming the file and the reason. Proven by negative control in both directions. - AiContentSectionTest asserted floors (Count >= 13 / >= 15). Now asserts the embedded fallback ships EXACTLY the authored files — a divergence there is invisible in dev (which reads the on-disk section) and only shows up as missing content in a shipped offline build. - Five stream-count assertions in the observable-query suites were tautologies: after Match(acc.Count >= N), Scan guarantees the snapshot has exactly N, so HaveCount(N) could never fail. Two tests had that as their ONLY assertion and therefore asserted nothing. Worse, the Count >= N gate they rode on was satisfied by an unrelated framework `_Access` satellite emission — ScopeSubtree was declaring success without ever observing the descendant. Now gated and asserted on emission SHAPE, per AGENTS.md's ban on "exactly N change events". - MeshPluginTest's HaveCount(5)/(13) were environment-dependent (RunTests registers only inside a repo checkout) and named nothing on failure. Replaced with the read-only allowlist as a subset assertion plus uniqueness. - DataCubesDocBlocksTest pinned the count of executable blocks in a shipped doc page; a sixth demo would have redded CI editorially. New shared helper AiContentSection carries the section invariants so the next content/ai/<Section> gets them for free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens AI-content loading and modernizes several tests to assert stable behavioral contracts/invariants rather than brittle or tautological snapshot counts, while ensuring malformed built-in skill files are reported instead of disappearing silently.
Changes:
- Add actionable parse diagnostics for skill markdown (front matter) and record/log skipped built-in skill files during catalog load.
- Introduce shared AI content section invariants in tests and replace multiple count-based assertions with shape/contract assertions.
- Relax documentation demo-block tests to avoid editorial breakage while still asserting key invariants (e.g., unique block ids).
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/MeshWeaver.Query.Test/ObservableQueryTests.cs | Replaces a tautological emission-count assertion with a path-shape contract. |
| test/MeshWeaver.Query.Test/ObservableQueryIntegrationTests.cs | Reworks multiple observable-query tests to assert on emitted content shape rather than counts/racy sampling. |
| test/MeshWeaver.Persistence.Test/DataCubesDocBlocksTest.cs | Removes brittle exact-count pin; asserts non-empty and uniqueness invariant for executable block ids. |
| test/MeshWeaver.AI.Test/SkillNodeTypeTest.cs | Stops hardcoding built-in skill membership; keeps per-skill action contract assertions. |
| test/MeshWeaver.AI.Test/SkillHarnessImportSourceTest.cs | Asserts imported skill ids match the provider-derived catalog, avoiding duplicated literals. |
| test/MeshWeaver.AI.Test/MeshPluginTest.cs | Converts tool-surface tests from count assertions to explicit allowlist/subset invariants. |
| test/MeshWeaver.AI.Test/BuiltInSkillCatalogTest.cs | New catalog guard: file-set equality, no-skipped-file invariant, and negative controls for reporting. |
| test/MeshWeaver.AI.Test/BuiltInAgentContentTest.cs | Updates malformed-front-matter guard to assert an actionable reported error (not just null). |
| test/MeshWeaver.AI.Test/AiContentSectionTest.cs | Asserts embedded fallback content matches authored on-disk section exactly (set equality). |
| test/MeshWeaver.AI.Test/AiContentSection.cs | New shared helper encoding AI content section invariants used by multiple tests. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-11-broken-skill-file-no-longer-vanishes-silently.md | What’s New entry describing the new “broken skill file is reported” behavior. |
| src/MeshWeaver.AI/SkillMarkdown.cs | Adds TryParse with actionable error output while keeping Parse API. |
| src/MeshWeaver.AI/BuiltInSkillProvider.cs | Records skipped files as AiContentLoadFailure and logs them to stderr; exposes catalog failures for tests. |
| src/MeshWeaver.AI/BuiltInAgentProvider.cs | Adds TryParseEmbeddedNode returning an error reason for guard tests; keeps skip-on-error behavior. |
| src/MeshWeaver.AI/AiContentLoadFailure.cs | New shared failure record type for reporting skipped AI content files. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| var yamlBlock = document.Descendants<YamlFrontMatterBlock>().FirstOrDefault(); | ||
| if (yamlBlock == null) return null; | ||
| if (yamlBlock == null) | ||
| { | ||
| error = "no YAML front matter — the file must open with a '---' fenced block"; | ||
| return null; | ||
| } |
| foreach (var resourceName in assembly.GetManifestResourceNames() | ||
| .Where(n => n.StartsWith(skillPrefix, StringComparison.Ordinal) | ||
| && n.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) | ||
| .Order()) | ||
| { |
Test Results (shard 0)707 tests 707 ✅ 14m 10s ⏱️ Results for commit 242f842. ♻️ This comment has been updated with latest results. |
Test Results (shard 3) 11 files 11 suites 8m 3s ⏱️ Results for commit 242f842. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)1 230 tests 1 230 ✅ 6m 57s ⏱️ Results for commit 242f842. ♻️ This comment has been updated with latest results. |
Test Results (shard 5)1 797 tests 1 605 ✅ 6m 29s ⏱️ Results for commit 242f842. ♻️ This comment has been updated with latest results. |
Test Results (shard 4)1 304 tests 1 294 ✅ 12m 13s ⏱️ Results for commit 242f842. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)2 089 tests 2 087 ✅ 6m 27s ⏱️ Results for commit 242f842. ♻️ This comment has been updated with latest results. |
Test Results 65 files 65 suites 54m 21s ⏱️ Results for commit 242f842. ♻️ This comment has been updated with latest results. |
…a75c720900f # Conflicts: # test/MeshWeaver.AI.Test/SkillHarnessImportSourceTest.cs # test/MeshWeaver.AI.Test/SkillNodeTypeTest.cs
MeshPluginTest.FullCrudWorkflow_CreateGetUpdateDelete red on CI, and the assertion
named the mechanism exactly: after Delete was acknowledged, the verifying Get
returned the node intact at version 2 instead of "Not found".
A mutation's ack ("Created:"/"Updated:"/"Deleted:") means the write was ACCEPTED,
not that every reader has caught up. Get reads through the per-node
MeshNodeStreamCache handle, which can still be serving the pre-mutation snapshot
when the ack returns. "Still the old value" is therefore a legitimate intermediate
state, and asserting a terminal one off a SINGLE read asserts that a race resolved
in your favour — the same defect its sibling GetDiagnostics_BrokenNodeType_
ReturnsErrorStatus was fixed for.
Fixed by waiting for the CONDITION — never a sleep, never a widened threshold,
never a re-run — via the house pattern for a request/response source (AGENTS.md
"Never Task.Delay to wait for propagation"; cf. CrossPartitionMoveCopyAccess
ContextTest.WaitUntil). Applied to ALL THREE read-after-write points, not just the
one that fired: create-then-read and update-then-read carry the identical race.
The timeout names what never happened, so a real regression reads as a diagnosis.
Also dropped the now-implied NotStartWith("Not found") — the poll predicate
guarantees it, and this branch is about not shipping assertions that cannot fail.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflict resolution: accept main's deletion of BuiltInSkillCatalog.cs and its file-derived skill expectations (#1225) — the share-email id I had added to that literal is now derived from content/ai/Skill/*.md, so nothing to carry over.
MeshWeaver ships dual-licensed Apache-2.0 / MIT. A copyleft or pay-to-use dependency is therefore a licensing defect, and nothing in the build checked for one: a licence is added by a single line in
Directory.Packages.props, the compiler is happy, and every test still passes.This audits all 432 packages in the dependency graph — direct and transitive — deletes the one free win, and adds a gate so it cannot regress silently.
What the audit found
itext79.6.0QuestPDF2026.7.2JsonPatch.Net,JsonPath.Net,Json.More.Net,JsonPointer.NetUglyToad.PdfPig(+4 sub-packages)<license>element at allSpot-checked and clean, as expected:
Radzen.Blazor(MIT — the component library, not the commercial IDE),DocSharp.Markdown,BlazorMonaco,ClaudeAgentSdk,CSharpVitamins.ShortGuid,CommunityToolkit.Maui,akgul.Maui.DataGrid,LiveChartsCore(all MIT);Appium.WebDriver,Castle.Core,ModelContextProtocol*,SQLitePCLRawmanaged (Apache-2.0);SQLitePCLRaw.lib.e_sqlite3(SQLite, public domain).itext7was dead configurationIt is AGPL-3.0-or-commercial — the more serious of the two, since AGPL is viral for a network-served product. It turned out to have no
PackageReferenceanywhere: not in any.csproj/.props/.targets, nousing iText, no#r "nuget:"directive, and nothing in the in-mesh C# thatdotnet buildcannot see (node JSON,.csxtemplates,configurationlambdas). Decisively, noproject.assets.jsoncontains anitext7/9.6.0library entry — it appears only insidecentralPackageVersions, a verbatim echo of the declaration. It was never resolved, never downloaded, never shipped.So deleting it was free. But nothing would have told us either way, which is the actual problem this PR fixes.
QuestPDF— removal is real work, not a line deletionPer the user's decision it is being dereferenced (not licensed, not swapped for another restrictive package); PDF rendering moves to the browser via the existing MIT/Playwright
Pixel/HeadlessChromiumPdfRenderer. Its footprint is small — one renderer (Pdf/PdfDocumentRenderer.cs, 368 lines), one licence-acceptance line, onePackageReference— but it cannot be deleted in isolation today:ExportPdf.csx:193-196falls back to QuestPDF for every non-Deck node, and the Chromium path has no cover page, TOC, running header/footer or page-break rules (PixelFaithfulExport.md:70).Deleting it now would be a functional regression, not a cleanup, so it lands with the document-export migration — #1230, coordinated with the agent doing that work. This PR touches no file under
src/MeshWeaver.Markdown.Export.json-everything— a decision, not a bugFour packages carry
OSMFEULA.txt. This is not the QuestPDF shape and the difference matters: the source is MIT, the EULA says explicitly "The Fee is not a license fee", and "the OSI License shall govern" on conflict. It is a maintenance-fee expectation on the published binary for revenue-generating users ≥ US$10,000/yr.JsonPatch.Netalso implements the RFC 7396 merge-patch path behind every cross-hubstream.Update, so replacing it touches the core mutation API. Raised as #1231 rather than decided unilaterally.The gate
.github/scripts/check-licenses.py, run by the newDependency licencesjob.project.assets.json), so transitive dependencies are covered — a copyleft library that arrives indirectly binds us just as hard. It caught thejson-everythingtransitives (Json.More.Net,JsonPointer.Net) that a declared-packages-only check would have missed..nuspec— SPDX expression, else by classifying the licence file. Deny patterns are tested first: a file granting MIT-style permissions that also mentions the Affero GPL classifies as AGPL. That ordering is the difference between catching a relicensed package and waving it through.<PackageVersion>no project references restores nothing and is redistributed nowhere — dead config, reported but not failed on. There are 40 such entries (this is whatitext7was).Gate shape (AGENTS.md)
continue-on-error, no input-shapedif:. It needs no secret, so it has no legitimate skip and no fork exemption — it runs on fork PRs too.collect-results(the only required check) with an explicit fail step, becauseneeds:alone does not fail analways()job.Verified the gate actually fails — a gate that cannot fail is decorative. With
EXCEPTIONScleared it exits 1 and names all 5 violations; classification was checked against real AGPL/GPL/LGPL/OSMF/dual-commercial/MIT/Apache texts, including the MIT-text-inside-an-AGPL-file trap.Verification
dotnet restoreclean after theitext7removal (a missingPackageVersionfor a referenced package is a restore error, so this is the real proof nothing referenced it).dotnet build src/MeshWeaver.Documentation -c Release -warnaserrorclean.DocumentationLinkIntegrityTest+WhatsNewEntryIntegrityTestpass.Docs:
Doc/Architecture/DependencyLicensing(policy, allowlist, exception rules). What's New entry included (Category: Fix).Follow-ups: #1230 (remove QuestPDF), #1231 (decide json-everything). Each closes by deleting its
EXCEPTIONSentry, at which point the gate enforces it permanently.