test(ci): first engine-free test project (Tests~/EngineFree) + a hosted-ubuntu job - #976
Merged
Merged
Conversation
…ed-ubuntu job The Unity plugin had ZERO engine-free tests: every C# signal came from a game-ci Unity Test Runner leg (licence + editor boot + minutes, and nothing at all on a fork PR). This adds a plain Microsoft.NET.Sdk xUnit project that compiles a chosen set of the REAL plugin sources via <Compile Include> and runs in seconds on ubuntu-latest, on the Godot-MCP.Tests model. Covered (44 tests): - NuGet resolver pins + the asmdef generation gate of issue #957, with the rule of .github/scripts/check_nuget_gate.py re-implemented in C# over the compiled-in NuGetConfig constants (not by shelling out to Python), so the two guards fail for genuinely different reasons. - Cloud authorization-server connection config (DeviceAuthService: base-URL and endpoint derivation, the mcp:agent LOGIN scope, the RFC 8628/6749 wire forms and documents, base64url JWT subject decoding) plus the plugin log-level gate. - [AiToolType]/[AiTool] plumbing, driven through the REAL McpPluginBuilder so the System vs Standard registry split is observed rather than asserted from the source text. The csproj consumes McpPlugin 8.3.0 / ReflectorNet 5.4.0 — the same pins NuGetConfig.cs installs — which makes it a SECOND pin location. A parity test compares the versions MSBuild actually restored (exported as assembly metadata) against NuGetConfig.Packages, so a missed bump fails loudly instead of silently testing yesterday's DLLs. The job passes RunConfiguration.TreatNoTestsAsError=true because, measured on this project, `dotnet test` exits 0 on a zero-test run — without the flag a vacuous suite would report a green job. Tests~/ keeps the project invisible to Unity (no import, no .meta, no csproj regeneration) and outside the UPM package release.yml packs; .gitignore:35 re-includes just this csproj past the `*.csproj` rule that covers Unity's generated project files. Unity-MCP-Plugin/Tests~/EngineFree/README.md records the files that were measured NOT to compile engine-free and the two UnityEngine leaves they all funnel through, so the next person does not rediscover it by grep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SHsWZUyyCcq4D78nXkXX2T
The plant round caught this: `{"sub":"user_42?/+"}` — chosen to look like it
exercised the base64url substitution because the literal characters are right
there in the subject — encodes to base64 containing NEITHER '+' nor '/', so the
fixture exercised neither Replace() call and scored GREEN with both of them
deleted. Readability and discriminating power were pulling in opposite
directions and readability won.
The subject is now selected by search for the property that does the work, and
the test asserts that property ('-' and '_' present in the encoded payload)
before decoding, so a future edit that makes the fixture toothless again fails
loudly instead of passing for free.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHsWZUyyCcq4D78nXkXX2T
Temporary. Proves the engine-free-tests job cannot report green on a vacuous suite. Measured locally first: `dotnet test` on this project with ZERO tests exits 0 WITHOUT RunConfiguration.TreatNoTestsAsError=true and exits 1 WITH it, so the flag is load-bearing rather than decorative. This commit reproduces that in CI; the next commit reverts it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SHsWZUyyCcq4D78nXkXX2T
This reverts commit 3d276c3.
…ity guard DoD 7, run rather than assumed. With a repo-root Directory.Build.targets + nuget.config redirecting at a local feed of repacked -ws.g<sha8> packages and UseWorkspaceSources=true, EngineFree restores com.IvanMurzak.McpPlugin/8.3.0-ws.g9c0e11d2 (+ Common, + ReflectorNet 5.4.0-ws.g1dff5501) into obj/project.assets.json, and all 44 tests pass against those packages — also under -p:Version, so the <Compile Include> links are unaffected by either. The one thing worth writing down: the pin-parity guard stays GREEN under the override, because Directory.Build.targets is imported AFTER the ItemGroup that captures the metadata, so it reports the DECLARED pin rather than the resolved one. That is what this guard should do — it is a check about what the release train must bump, and a transient override must not redden it — but the earlier comment said "the versions MSBuild actually restored", which is not what it reads. Corrected in the csproj, the test and the README. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SHsWZUyyCcq4D78nXkXX2T
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Unity had zero engine-free tests. Every C# signal came from a game-ci Unity Test Runner leg — licence, editor boot, minutes, and nothing at all on a fork PR. This adds a plain
Microsoft.NET.SdkxUnit project (Unity-MCP-Plugin/Tests~/EngineFree) that compiles a chosen set of the real plugin sources via<Compile Include>and runs in seconds onubuntu-latest, on theGodot-MCP.Testsmodel, plus theengine-free-testsjob that runs it.44 tests, ~65 ms.
CI evidence
3d276c3e(all tests deleted)engine-free tests (.NET 8)FAILED —No test is available ... Process completed with exit code 1, whilenuget gate, bothtest-clilegs and the completed Unity legs stayed green. Reverted by7fbf3527.519a55faengine-free tests (.NET 8)SUCCESS —Passed! - Failed: 0, Passed: 44, Skipped: 0, Total: 44, Duration: 230 ms, whole job 22 s onubuntu-latest/ SDK 8.0.xp0-unity-ci-regreen's final run, and 33815830447 11 h latertest-unity-6000-3-1f1-editmode (base)flaked once on run 33863846659 withFailed to resolve packages: Access to 'https://packages.unity.com/com.unity.ide.visualstudio' was denied. No packages loaded.inside the game-ci container — a Unity package-registry denial, green on both baseline runs above, and re-run to confirm. Two independent facts say this diff cannot reach it: the log contains zeroCS####errors and zero occurrences ofEngineFree, i.e. Unity never imported the new folder at all.DoD 7 — workspace-source override, run rather than assumed
With a repo-root
Directory.Build.targets(the02§5 fragment shape) +nuget.configpointing at a local feed of repacked-ws.g<sha8>packages andUseWorkspaceSources=true,obj/project.assets.jsonresolves:All 44 tests pass against those packages, and again with
-p:Version=8.3.0-ws.g9c0e11d2— so theGodot-MCP.Tests-style<Compile Include>links are unaffected by either. The project stays override-compatible by construction: noUseWorkspaceSourcesproperty of its own, nopackages.lock.json, noRestorePackagesPath, nonuget.config.One measured detail worth knowing: the pin-parity guard stays GREEN under the override.
Directory.Build.targetsis imported AFTER theItemGroupthat captures the metadata, so the metadata reports the DECLARED pin, not the resolved one. That is what this guard should do — it is a check about what the release train must bump, and a transient override must not redden it — but it does mean the guard is not a proof of what was restored. Said plainly in the csproj, the test and the README rather than left to be rediscovered.What is covered
NuGetPinGateTests.cs.github/scripts/check_nuget_gate.py's rule re-implemented in C# over the compiled-inNuGetConfigconstants (not by shelling out to Python), so the two guards fail for genuinely different reasons: the script parsesNuGetConfig.csas text with regexes, these read what the compiler seesConnectionConfigTests.csmcp:agentLOGIN scope (never narrowed tomcp:plugin, the P0-3 hazard), the RFC 8628/6749 wire forms and documents, base64url JWT subject decoding — plus the plugin log-level gateToolAttributeTests.cs[AiToolType]/[AiTool]driven through the realMcpPluginBuilder, so the System-vs-Standard registry split is observed rather than asserted from the source textScope limits, measured rather than assumed
Tests~/EngineFree/README.mdrecords this in full. The short version: Unity's engine-free surface is far smaller than Godot's, and almost all of it funnels through two leaves —Runtime/Logger/UnityLogger.cs(UnityEngine.Debug.*) andRuntime/Unity/Logs/UnityLogCollector.cs(UnityEngine.Application.logMessageReceivedThreaded)."0
UnityEnginegrep hits" is a hypothesis about compilability, not the answer — a file drags in the engine transitively just as fatally. Verified by building, not by reading:Runtime/Utils/EnvironmentUtils.csCS0234: namespace 'Utils' does not existUnityLoggerFactorytoUnityLoggerProvidertoUnityLoggerRuntime/UnityMcpPlugin.csCS0246: UnityLogCollectorUnityLogCollectorRuntime/UnityMcpPlugin.Config.csCS0103: GeneratePortFromDirectory/EnvironmentUtilsRuntime/UnityMcpPluginBuilder.csCS0246: UnityMcpPluginRuntimeUnityMcpPlugintoUnityMcpPlugin.Build.cs(using UnityEngine)Editor/DependencyResolver/NuGetInstallManifest.csCS0246: UnityEngineDebug.LogWarning, not behind#if UNITY_EDITOREditor/DependencyResolver/NuGetPackageRestorer.csCS0246: UnityEngineDebug.Log*calls, sameSo, stated plainly rather than papered over:
UnityConnectionConfigandEnvironmentUtils.ApplyEnvironmentOverridesare NOT covered here. Theargs > env > diskladder stays a game-ci EditMode concern. Area (b) covers the parts of connection configuration that are reachable.Tool_Pingof the tool families is covered. It is the only[AiToolType]class whose every partial file is engine-free. A partial class assembled from a subset of its files presents a surface that does not exist in production, so a reflection test over it would be measuring the wrong object and reporting green — the csproj comments make that a rule, not a preference.Nothing is stubbed. A file that does not compile is left out and named.
New pin location — for the release train
EngineFree.csprojconsumescom.IvanMurzak.McpPlugin8.3.0 andcom.IvanMurzak.ReflectorNet5.4.0 — the same pinsNuGetConfig.cs:117,124installs. That makes this csproj a second pin location the train must bump (p3-train-create,update-consumers.py:114-135B path). Filed as a ROADMAP note for sibling C, not edited here.It cannot rot silently:
PinnedPackageVersions_MatchNuGetConfigcompares the declared versions — exported asAssemblyMetadatafrom the realPackageReferenceitems rather than re-typed as a literal, so the guard cannot drift from the csproj — againstNuGetConfig.Packages. (What it does not prove is what restore resolved; see the DoD 7 note above.)Incidental finding: the mirror-image downgrade (
csprojReflectorNet to 5.3.3) is not possible — McpPlugin 8.3.0 requires>= 5.4.0transitively and NuGet refuses withNU1605before any test runs. So the csproj can drift upward but not below the framework floor.Vacuity guards
RunConfiguration.TreatNoTestsAsError=trueis load-bearing, not decoration. Measured on this project: with all tests deleted,dotnet testexits 0 without the flag and 1 with it. Without it a vacuous suite reports a green job. (Reproduced in CI by the DoD-5 plant run above.)sub = "user_42?/+"— chosen because the literal characters look like the thing under test. Its base64 contains neither+nor/, so it exercised neitherReplace()and scored GREEN under the plant with both deleted. The subject is now selected by search for the property that does the work, and the test asserts-and_are present in the encoded payload before decoding, so it cannot go toothless again.Plant table — 35 plants, 35 RED, all 32 test methods covered
Each row: an exact one-occurrence source mutation, applied to a clean tree, diff-verified, suite run, then reverted with the tree re-asserted clean. Baseline before every plant:
44/44 passed.PinnedPackageVersions_MatchNuGetConfigPinnedPackageVersions_MatchNuGetConfig,PinnedSet_MatchesTheBlessedLock_AndTheLockPairsTheCurrentGenerationDefinePinnedPackageVersions_MatchNuGetConfigPinnedPackageVersions_MatchNuGetConfig,PinnedSet_MatchesTheBlessedLock_AndTheLockPairsTheCurrentGenerationDefinePinnedSet_MatchesTheBlessedLock_AndTheLockPairsTheCurrentGenerationDefineGenerationDefine_IsPresent_PrefixedAndDistinctFromTheReadyDefine,PinnedSet_MatchesTheBlessedLock_AndTheLockPairsTheCurrentGenerationDefineGateDefines_AreExactlyTheReadyAndGenerationPairPackages_AreNonEmpty_UniquelyIdentified_AndEveryVersionParses,PinnedSet_MatchesTheBlessedLock_AndTheLockPairsTheCurrentGenerationDefinePackages_AreNonEmpty_UniquelyIdentified_AndEveryVersionParses,PinnedSet_MatchesTheBlessedLock_AndTheLockPairsTheCurrentGenerationDefineDigest_AgreesWithThePythonCanonicalForm_ForTheBlessedPinListDigest_AgreesWithThePythonCanonicalForm_ForTheBlessedPinList,Digest_IsOrderInsensitive_ButChangesWhenAnyPinVersionChanges,PinnedSet_MatchesTheBlessedLock_AndTheLockPairsTheCurrentGenerationDefineDigest_AgreesWithThePythonCanonicalForm_ForTheBlessedPinList,Digest_IsOrderInsensitive_ButChangesWhenAnyPinVersionChanges,PinnedSet_MatchesTheBlessedLock_AndTheLockPairsTheCurrentGenerationDefineDownloadUrl_IsTheLowerCasedFlatContainerPathCachedPath_IsUnderTheConfiguredCacheDirectoryTargetFrameworkPriority_PrefersNetstandard21_AndEndsWithTheRootLibFallbackEndpointUrls_AreAppendedToTheTrimmedBaseConstructor_TrimsTheBase_DefaultsTheClientId_AndRejectsAnEmptyBaseDeviceLogin_RequestsTheAgentScope_AndIsNeverNarrowedToThePluginScopeDeviceAuthorizeForm_CarriesTheClientIdAndScope_UnderTheRfc8628FieldNamesDeviceTokenForm_RedeemsTheDeviceCodeUnderTheDeviceCodeGrantUrnDeviceAuthorizeResponse_ReadsTheSnakeCaseDocumentDeviceTokenResponse_DistinguishesASuccessFromAnRfc6749PendingErrorJwtSubject_IsDecodedFromTheBase64UrlPayloadJwtSubject_ReturnsNullOnAnythingMalformedJwtSubject_ReturnsNullWhenTheClaimIsAbsentOrNotAStringLogLevelGate_EmitsOnlyAtOrAboveTheConfiguredThresholdLogLevelOrder_RunsFromTraceToNone_SoTheGateComparisonMeansWhatItSaysAssemblyScan_DiscoversPingAndLandsItOnTheSystemRegistry,EveryAiToolOnACompiledInFamily_DeclaresANameAndAKnownToolType,ToolClass_CarriesAiToolType_SoTheAssemblyScanCanFindItToolIdConstant_IsTheNameTheAttributeActuallyRegistersAssemblyScan_DiscoversPingAndLandsItOnTheSystemRegistry,ExplicitRegistration_ByType_ProducesTheSameSystemToolEntry,PingIsRegisteredAsASystemTool_NotAStandardOnePingCarriesTheReadOnlyAndIdempotentHints_AndStaysEnabledPingCarriesSkillDescriptionAndBody_SoTheGeneratedSkillMdIsUsefulAssemblyScan_DiscoversPingAndLandsItOnTheSystemRegistry,EveryAiToolOnACompiledInFamily_DeclaresANameAndAKnownToolType,ExplicitRegistration_ByType_ProducesTheSameSystemToolEntry,ToolIdConstant_IsTheNameTheAttributeActuallyRegistersAssemblyScan_RegistersNothing_WhenTheHostingAssemblyIsIgnoredBuilderWithNoToolsOptedIn_RegistersNothingEvery one of the 32 distinct test methods is turned RED by at least one plant. P11/P12/P34/P35 mutate the test-side code that is the thing under test (the digest re-implementation; the two negative controls) — labelled as such rather than dressed up as product mutations.
Invisible to Unity and to UPM
git ls-files Unity-MCP-Plugin/Tests~/EngineFreereturns the csproj + 3 test files + README + a local.gitignore. Nothing else..gitignore:35!Tests~/EngineFree/*.csprojre-includes exactly this csproj past the*.csprojrule that exists for Unity's generated project files;git check-ignore -vconfirmsAssembly-CSharp.csprojis still ignored.grep -r EngineFree Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcpreturns 0 hits (that dir is whatrelease.yml:213,250packs).Assets/andPackages/and ends in~, so Unity never imports it:find Unity-MCP-Plugin/Tests~ -name '*.meta'returns 0.One deliberate deviation from the task's file list, flagged for review: the task said to touch
Unity-MCP-Plugin/.gitignoreand "nothing else" there, which I did — but nothing in the repo ignoresbin/,obj/orTestResults/under this new folder (the plugin-root/[Oo]bj/rule is rooted atUnity-MCP-Plugin/), so a localdotnet testfollowed bygit add -Awould commit the whole restore output. I added a 6-lineTests~/EngineFree/.gitignorescoped entirely inside the folder this task creates, rather than widening the plugin-root file.🤖 Generated with Claude Code
https://claude.ai/code/session_01SHsWZUyyCcq4D78nXkXX2T