From 625efe698010f0228d737f7e0cf541c5d0115454 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 28 Jul 2026 23:25:57 +0100 Subject: [PATCH 01/32] fix: remove dangling asmdef GUID reference GUID:ffab5256b265d45fa9fb86697af7ee2b resolves to no .asmdef anywhere in this repo or Library/PackageCache. The other two GUIDs in this reference list (Addressables, Addressables.Editor) are confirmed live; this third one was orphaned, likely from a removed dependency. Co-Authored-By: Claude Sonnet 5 --- Editor/GameLovers.Services.Editor.asmdef | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Editor/GameLovers.Services.Editor.asmdef b/Editor/GameLovers.Services.Editor.asmdef index ed041e6..3f197da 100644 --- a/Editor/GameLovers.Services.Editor.asmdef +++ b/Editor/GameLovers.Services.Editor.asmdef @@ -5,8 +5,7 @@ "GameLovers.Services", "GameLovers.GameData", "GUID:9e24947de15b9834991c9d8411ea37cf", - "GUID:69448af7b92c7f342b298e06a37122aa", - "GUID:ffab5256b265d45fa9fb86697af7ee2b" + "GUID:69448af7b92c7f342b298e06a37122aa" ], "includePlatforms": [ "Editor" From 944602c15a7e0eddf1d8854a084120bcc9b3f488 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Wed, 29 Jul 2026 00:38:09 +0100 Subject: [PATCH 02/32] fix: converge repo URLs, sample description, and asmdef naming/consistency Bumps to 2.1.2 (published at 2.1.1; this is a real release, not a fold). - README: converged repository links on the actual origin (github.com/CoderGamester/Services) -- was inconsistently mixed with com.gamelovers.services, which is not this repo's name. Also fixed the cross-package GameData link (actual origin is Unity-GameData). - package.json: ServicesPlayground sample description no longer claims the UI is built programmatically -- it ships as a hand-authored prefab with a [SerializeField]-wired driver script. - Renamed Tests/EditMode/GameLovers.Services.Tests.asmdef to GameLovers.Services.Editor.Tests.asmdef to match its own `name` field; GUID preserved via git mv on the paired .meta. - Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef no longer sets autoReferenced: true (was the only test asmdef in the repo doing so). Verified safe: the Test Runner discovers test assemblies via the UNITY_INCLUDE_TESTS define constraint, independent of autoReferenced -- full PlayMode suite still finds and runs this assembly's tests (305/305 passing, same count as before the change). Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 11 +++++++++++ README.md | 12 ++++++------ ...smdef => GameLovers.Services.Editor.Tests.asmdef} | 0 ... => GameLovers.Services.Editor.Tests.asmdef.meta} | 0 .../GameLovers.Services.Tests.Playmode.asmdef | 2 +- package.json | 4 ++-- 6 files changed, 20 insertions(+), 9 deletions(-) rename Tests/EditMode/{GameLovers.Services.Tests.asmdef => GameLovers.Services.Editor.Tests.asmdef} (100%) rename Tests/EditMode/{GameLovers.Services.Tests.asmdef.meta => GameLovers.Services.Editor.Tests.asmdef.meta} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58827d1..db0865b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [2.1.2] - 2026-07-29 + +**Fixed**: +- Removed a dangling asmdef GUID reference (`GUID:ffab5256b265d45fa9fb86697af7ee2b`) from `Editor/GameLovers.Services.Editor.asmdef` — resolved to no assembly anywhere in the repo or `Library/PackageCache`. +- Renamed `Tests/EditMode/GameLovers.Services.Tests.asmdef` to `GameLovers.Services.Editor.Tests.asmdef` to match its own `name` field (`GameLovers.Services.Editor.Tests`); GUID preserved via `git mv` on the paired `.meta`. +- `Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef` no longer sets `autoReferenced: true` (was the only test asmdef in the repo doing so). Test discovery is unaffected — the Test Runner finds test assemblies via the `UNITY_INCLUDE_TESTS` define constraint, independent of `autoReferenced`. + +**Docs**: +- `Samples~/ServicesPlayground`'s `package.json` description no longer claims the sample UI "is built programmatically" — it ships as a hand-authored prefab (`ServicesPlaygroundUI.prefab`) with a `[SerializeField]`-wired driver script. +- Converged `README.md`'s repository links on the actual origin (`github.com/CoderGamester/Services`) — previously mixed with `com.gamelovers.services`, which is not this repository's name. Also fixed the GameData cross-reference link (actual origin is `Unity-GameData`, not `com.gamelovers.gamedata`). + ## [2.1.1] - 2026-07-04 **Changed**: diff --git a/README.md b/README.md index 8fda8bf..bf97dcf 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Building robust game architecture in Unity often leads to tightly coupled system ## System Requirements - **[Unity](https://unity.com/download)** 6000.0+ (Unity 6) -- **[GameLovers GameData](https://github.com/CoderGamester/com.gamelovers.gamedata)** (v1.0.0) — automatically resolved +- **[GameLovers GameData](https://github.com/CoderGamester/Unity-GameData)** (v1.0.0) — automatically resolved - **[Unity Addressables](https://docs.unity3d.com/Packages/com.unity.addressables@latest)** (≥ 1.21.20) — automatically resolved - **[UniTask](https://github.com/Cysharp/UniTask)** (≥ 2.5.10) — automatically resolved @@ -51,14 +51,14 @@ Building robust game architecture in Unity often leads to tightly coupled system 1. Open Unity Package Manager (`Window` → `Package Manager`) 2. Click `+` → `Add package from git URL` -3. Enter: `https://github.com/CoderGamester/com.gamelovers.services.git` +3. Enter: `https://github.com/CoderGamester/Services.git` ### Via manifest.json ```json { "dependencies": { - "com.gamelovers.services": "https://github.com/CoderGamester/com.gamelovers.services.git" + "com.gamelovers.services": "https://github.com/CoderGamester/Services.git" } } ``` @@ -323,7 +323,7 @@ To import a sample: **Window > Package Manager > GameLovers Services > Samples > ## Contributing -Contributions are welcome! See [GitHub Issues](https://github.com/CoderGamester/com.gamelovers.services/issues) to report bugs or request features. For development setup, architecture details, namespace conventions, and coding standards, see [AGENTS.md](AGENTS.md). +Contributions are welcome! See [GitHub Issues](https://github.com/CoderGamester/Services/issues) to report bugs or request features. For development setup, architecture details, namespace conventions, and coding standards, see [AGENTS.md](AGENTS.md). --- @@ -338,8 +338,8 @@ Contributions are welcome! See [GitHub Issues](https://github.com/CoderGamester/ ## Support -- **Issues**: [Report bugs or request features](https://github.com/CoderGamester/com.gamelovers.services/issues) -- **Discussions**: [Ask questions and share ideas](https://github.com/CoderGamester/com.gamelovers.services/discussions) +- **Issues**: [Report bugs or request features](https://github.com/CoderGamester/Services/issues) +- **Discussions**: [Ask questions and share ideas](https://github.com/CoderGamester/Services/discussions) ## License diff --git a/Tests/EditMode/GameLovers.Services.Tests.asmdef b/Tests/EditMode/GameLovers.Services.Editor.Tests.asmdef similarity index 100% rename from Tests/EditMode/GameLovers.Services.Tests.asmdef rename to Tests/EditMode/GameLovers.Services.Editor.Tests.asmdef diff --git a/Tests/EditMode/GameLovers.Services.Tests.asmdef.meta b/Tests/EditMode/GameLovers.Services.Editor.Tests.asmdef.meta similarity index 100% rename from Tests/EditMode/GameLovers.Services.Tests.asmdef.meta rename to Tests/EditMode/GameLovers.Services.Editor.Tests.asmdef.meta diff --git a/Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef b/Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef index 2a62ea1..3241f31 100644 --- a/Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef +++ b/Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef @@ -19,7 +19,7 @@ "precompiledReferences": [ "nunit.framework.dll" ], - "autoReferenced": true, + "autoReferenced": false, "defineConstraints": [ "UNITY_INCLUDE_TESTS" ], diff --git a/package.json b/package.json index 349e6ae..c640f29 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "com.gamelovers.services", "displayName": "GameLovers Services", "author": "Miguel Tomas", - "version": "2.1.1", + "version": "2.1.2", "unity": "6000.3", "license": "MIT", "description": "Foundation services for a Unity based project (DI-lite, messaging, ticking, coroutines, pooling, persistence, RNG, time, commands, versioning) plus Addressables-based asset loading and importing tooling.", @@ -16,7 +16,7 @@ "samples": [ { "displayName": "Services Playground", - "description": "Zero-setup playground that wires every foundation service via MainInstaller and drives the Services Explorer (Installer, Versioning, Message Broker, Tick, Coroutine, Pool, Data, Time, RNG, Commands). Open the scene and press Play — the UI is built programmatically; no per-import wiring.", + "description": "Zero-setup playground that wires every foundation service via MainInstaller and drives the Services Explorer (Installer, Versioning, Message Broker, Tick, Coroutine, Pool, Data, Time, RNG, Commands). Open the scene and press Play — the UI ships as a hand-authored prefab (ServicesPlaygroundUI.prefab) with a driver script holding [SerializeField] references; no per-import wiring.", "path": "Samples~/ServicesPlayground" }, { From 8a2506889d85b513b33a16d3b875485370a582c6 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Wed, 29 Jul 2026 15:48:13 +0100 Subject: [PATCH 03/32] docs: update the changelog to reflect the changes from the user perspective --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db0865b..c941530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [2.1.2] - 2026-07-29 **Fixed**: -- Removed a dangling asmdef GUID reference (`GUID:ffab5256b265d45fa9fb86697af7ee2b`) from `Editor/GameLovers.Services.Editor.asmdef` — resolved to no assembly anywhere in the repo or `Library/PackageCache`. - Renamed `Tests/EditMode/GameLovers.Services.Tests.asmdef` to `GameLovers.Services.Editor.Tests.asmdef` to match its own `name` field (`GameLovers.Services.Editor.Tests`); GUID preserved via `git mv` on the paired `.meta`. - `Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef` no longer sets `autoReferenced: true` (was the only test asmdef in the repo doing so). Test discovery is unaffected — the Test Runner finds test assemblies via the `UNITY_INCLUDE_TESTS` define constraint, independent of `autoReferenced`. **Docs**: - `Samples~/ServicesPlayground`'s `package.json` description no longer claims the sample UI "is built programmatically" — it ships as a hand-authored prefab (`ServicesPlaygroundUI.prefab`) with a `[SerializeField]`-wired driver script. -- Converged `README.md`'s repository links on the actual origin (`github.com/CoderGamester/Services`) — previously mixed with `com.gamelovers.services`, which is not this repository's name. Also fixed the GameData cross-reference link (actual origin is `Unity-GameData`, not `com.gamelovers.gamedata`). +- Converged `README.md`'s repository links on the actual origin. ## [2.1.1] - 2026-07-04 From e2f4761f9207e41fd9187c8861f1573dc69d5a43 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Wed, 29 Jul 2026 22:35:03 +0100 Subject: [PATCH 04/32] feat: give the ServicesPlayground sample its own assembly The sample uses #if ENABLE_INPUT_SYSTEM and UnityEngine.InputSystem.UI types while relying on Assembly-CSharp, which AGENTS.md 19 disallows. No editor asmdef is needed here -- the sample ships no editor scripts. References GameLovers.GameData directly. That one is not obvious: the sample reaches floatP through GameLovers.Services, but asmdef references are not transitive, so it fails with CS0012 without the direct reference. Caught by compiling the sample rather than by eyeballing the reference list -- Samples~ is invisible to Unity, so it was copied into Assets/ to build. Final state: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 --- ...Services.Samples.ServicesPlayground.asmdef | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Samples~/ServicesPlayground/GameLovers.Services.Samples.ServicesPlayground.asmdef diff --git a/Samples~/ServicesPlayground/GameLovers.Services.Samples.ServicesPlayground.asmdef b/Samples~/ServicesPlayground/GameLovers.Services.Samples.ServicesPlayground.asmdef new file mode 100644 index 0000000..abc0f91 --- /dev/null +++ b/Samples~/ServicesPlayground/GameLovers.Services.Samples.ServicesPlayground.asmdef @@ -0,0 +1,20 @@ +{ + "name": "GameLovers.Services.Samples.ServicesPlayground", + "rootNamespace": "GameLovers.Services.Samples.ServicesPlayground", + "references": [ + "GameLovers.Services", + "GameLovers.GameData", + "Unity.TextMeshPro", + "UnityEngine.UI", + "Unity.InputSystem" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} From fadc731bd5bf8d299751bd57ab8ae5b878a8b421 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Thu, 30 Jul 2026 00:34:14 +0100 Subject: [PATCH 05/32] fix: assign default actions when swapping in InputSystemUIInputModule Under Active Input Handling = "Input System Package (New)" the swap destroyed the scene's StandaloneInputModule and added an InputSystemUIInputModule via AddComponent, which leaves its actions unassigned. An unassigned module processes no input at all -- silently, with nothing logged -- so every button in the sample was dead. AssignDefaultActions() wires the default UI action map. Found while driving the sibling uiservice UrpRendering sample through the Unity MCP: buttons rendered, reported interactable, and did nothing on click. This sample shares the same helper, copied from it. Co-Authored-By: Claude Opus 5 --- Samples~/ServicesPlayground/ServicesPlaygroundUI.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Samples~/ServicesPlayground/ServicesPlaygroundUI.cs b/Samples~/ServicesPlayground/ServicesPlaygroundUI.cs index 16ee011..64703ab 100644 --- a/Samples~/ServicesPlayground/ServicesPlaygroundUI.cs +++ b/Samples~/ServicesPlayground/ServicesPlaygroundUI.cs @@ -182,7 +182,9 @@ private static void EnsureInputModuleOnEventSystem() { DestroyImmediate(legacy); } - go.AddComponent(); + // AddComponent leaves the module's actions unassigned, and an unassigned module silently + // processes no input at all -- every button appears dead with nothing logged. + go.AddComponent().AssignDefaultActions(); #else if (go.GetComponent() == null) { From beb16081fcbe15001b6f366f0bba189cca1323a1 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Sun, 2 Aug 2026 14:37:42 +0100 Subject: [PATCH 06/32] fix: pool disposal + fake-null spawn + addressable-id collisions; RCR-pin 14 tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three consumer-facing bugs found by the test-suite audit, each now pinned: - GameObjectPool.Dispose(bool) and GameObjectPool.Dispose(bool) destroyed SampleEntity unconditionally, ignoring disposeSampleEntity. - ObjectPoolBase.SpawnEntity could hand out a destroyed pooled object. Its retry loop tested `entity == null`, which inside a generic constrained only to `class` compiles to reference equality and never reaches UnityEngine.Object's overloaded == that detects a fake-null native object. Now dispatched via a runtime type check, falling back to reference equality for POCO pooled types. - AddressableIdsGeneratorUtils emitted duplicate enum members when two addresses sanitized to the same identifier, producing generated code that will not compile. The append path now uses the disambiguated name it already computed. AssetResolverService.Convert's type switch is extracted to an internal SelectAsset so the not-yet-loaded placeholder path is directly testable without an Addressables catalog; likewise BuildEnumSource in the generator. No behaviour change in either extraction. Also declares com.unity.test-framework.performance 3.5.0 (both test asmdefs already referenced Unity.PerformanceTesting unconditionally), and adds Tests/CLAUDE.md alongside the existing Tests/AGENTS.md. EditMode 830/830, PlayMode 299/299. RCR: GenerateEnumSource_TwoAddressesCollideAfterSanitization_ProducesDistinctMemberNames <- AddressableIdsGeneratorUtils.cs AppendAddressEnumMembers — revert to `stringBuilder.Append(GetCleanName(addresses[i], true));` RCR: GenerateEnumSource_AddressAppendedToInput_DoesNotRenumberExistingMembers <- AddressableIdsGeneratorUtils.cs AppendAddressEnumMembers — reverse the loop to `for (var i = addresses.Count - 1; i >= 0; i--)` RCR: GenerateEnumSource_EmptyAddressList_ProducesCompilableEmptyEnum <- AddressableIdsGeneratorUtils.cs BuildEnumSource — delete `stringBuilder.AppendLine("\t}");` RCR: SelectAsset_WhenReferenceNotDone_ReturnsErrorPlaceholderNotNull <- AssetResolverService.cs SelectAsset — invert the Sprite branch to `isDone ? errorSprite as TAsset : asset as TAsset` RCR: Spawn_OnPoolEntityObject_CallsInitWithOwningPoolEveryTime <- ObjectPool.cs CallInstantiator — delete `poolEntity?.Init(this);` RCR: Range_FloatMinEqualsMaxWithMaxInclusive_ReturnsMin <- RngService.cs Range — change `if (min > max || ...)` to `if (min >= max || ...)` RCR: Range_FloatMinEqualsMaxWithMaxExclusive_ThrowsIndexOutOfRange <- RngService.cs Range — drop the whole `|| (!maxInclusive && ...)` disjunct, leaving `if (min > max)` RCR: LoadVersionDataAsync_Successfully <- VersionServices.cs LoadVersionDataAsync — comment out `ApplyTextAsset(textAsset, asyncContext: true);` RCR: StopCoroutine_NullCoroutine_DoesNotThrow <- CoroutineService.cs StopCoroutine — remove the `coroutine == null ||` term from the guard RCR: StopCoroutine_AfterServiceObjectDestroyed_DoesNotThrowMissingReference <- CoroutineService.cs StopCoroutine — remove the `_serviceObject == null ||` term from the guard RCR: Dispose_WithDisposeSampleEntityFalse_DoesNotDestroySampleEntity <- GameObjectPool.cs Dispose — revert to unconditional `Object.Destroy(SampleEntity);` RCR: Spawn_WhenPooledEntityWasDestroyedExternally_ReturnsFreshInstance <- ObjectPool.cs SpawnEntity — collapse the do-while retry to a single unconditional pop RCR: Dispose_WithDisposeSampleEntityFalse_DoesNotDestroySampleEntity <- GameObjectPool.cs GameObjectPool.Dispose — revert to unconditional `Object.Destroy(SampleEntity.gameObject);` RCR: SubscribeOnUpdate_ZeroDeltaTimeWithOverflowToNextTick_TicksEveryFrame <- TickService.cs Update — drop the zero-guard, leaving `var overFlow = deltaTime % tickData.DeltaTime;` Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 634 +++++++++--------- .../AddressableIdsGeneratorUtils.cs | 44 +- Runtime/AssemblyInfo.cs | 1 + Runtime/AssetResolverService.cs | 101 +-- Runtime/Pooling/GameObjectPool.cs | 10 +- Runtime/Pooling/ObjectPool.cs | 16 +- Tests/AGENTS.md | 188 +++++- Tests/CLAUDE.md | 12 + Tests/CLAUDE.md.meta | 7 + .../Unit/AddressableIdsGeneratorTest.cs | 99 +++ .../Unit/AddressableIdsGeneratorTest.cs.meta | 2 + .../EditMode/Unit/AssetResolverServiceTest.cs | 22 + Tests/EditMode/Unit/DataServiceTest.cs | 15 +- Tests/EditMode/Unit/ObjectPoolTest.cs | 26 +- Tests/EditMode/Unit/RngServiceTest.cs | 28 + Tests/EditMode/Unit/VersionServicesTest.cs | 82 +-- .../VersionServicesIntegrationTest.cs | 86 +-- Tests/PlayMode/Unit/CoroutineServiceTest.cs | 30 + Tests/PlayMode/Unit/GameObjectPoolTest.cs | 34 + .../PlayMode/Unit/GameObjectPoolTypedTest.cs | 14 + Tests/PlayMode/Unit/TickServiceTest.cs | 25 + package.json | 3 +- 22 files changed, 923 insertions(+), 556 deletions(-) create mode 100644 Tests/CLAUDE.md create mode 100644 Tests/CLAUDE.md.meta create mode 100644 Tests/EditMode/Unit/AddressableIdsGeneratorTest.cs create mode 100644 Tests/EditMode/Unit/AddressableIdsGeneratorTest.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index c941530..2019aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,312 +1,322 @@ -# Changelog -All notable changes to this package will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - -## [2.1.2] - 2026-07-29 - -**Fixed**: -- Renamed `Tests/EditMode/GameLovers.Services.Tests.asmdef` to `GameLovers.Services.Editor.Tests.asmdef` to match its own `name` field (`GameLovers.Services.Editor.Tests`); GUID preserved via `git mv` on the paired `.meta`. -- `Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef` no longer sets `autoReferenced: true` (was the only test asmdef in the repo doing so). Test discovery is unaffected — the Test Runner finds test assemblies via the `UNITY_INCLUDE_TESTS` define constraint, independent of `autoReferenced`. - -**Docs**: -- `Samples~/ServicesPlayground`'s `package.json` description no longer claims the sample UI "is built programmatically" — it ships as a hand-authored prefab (`ServicesPlaygroundUI.prefab`) with a `[SerializeField]`-wired driver script. -- Converged `README.md`'s repository links on the actual origin. - -## [2.1.1] - 2026-07-04 - -**Changed**: -- PlayMode tests updated to parameterless `FindObjectsByType()` overload (Unity 6 API). -- Removed redundant `[Serializable]` from `AddressableConfig` (already implicitly serializable as a reference type in Unity YAML). - -**Fixed**: -- `ServicesScaffolders` adapts to Unity 6000.4+ `AssetCreationEndAction` / `EntityId` API (guarded by `UNITY_6000_4_OR_NEWER`; pre-6000.4 path unchanged). - -## [2.1.0] - 2026-05-20 - -**New**: -- `VersionServices` now auto-bootstraps via `[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]`, populating version metadata before any scene `Awake` callback and before vendor-SDK `SubsystemRegistration` callbacks that read it. Consumers no longer need to call `LoadVersionData()` / `LoadVersionDataAsync()` explicitly for the default flow. - -**Changed**: -- Property getters (`VersionInternal`, `Branch`, `Commit`, `BuildNumber`) now lazy-load via a new private `EnsureLoaded()` on first access if the auto-bootstrap hook has not yet fired — protects against undefined ordering between sibling assemblies' `[RuntimeInitializeOnLoadMethod]` callbacks at the same phase. -- Removed the private `IsLoaded()` helper (replaced by `EnsureLoaded()` invoked from each property getter). - -**Docs**: -- `docs/version-services.md` rewritten around the new auto-bootstrap contract: the recommended usage no longer includes any explicit load call, the lazy-load fallback is documented, and the Error Reference table now describes the fallback behaviour (no exception is raised). - -## [2.0.2] - 2026-05-20 - -**Fixed**: -- Add missing meta file - -## [2.0.1] - 2026-05-20 - -**New** -- Added new test suite for more rebust code coverage -- Added `VersionServices.LoadVersionData()` — synchronous sibling of `LoadVersionDataAsync()` for consumers who want to populate version metadata at boot without an `await`. Both methods now funnel into a shared private `ApplyTextAsset` helper, so behaviour is identical. Sync is the recommended default for the shipping `version-data.txt` (a few hundred bytes); async remains available for cases where `VersionData` is extended with large embedded blobs. Covered by `Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs`. - -**Fixed**: -- `Tests/AGENTS.md` §8 extended with a new **"Authorized reflection sites (storage-assertion exception)"** subsection. - -## [2.0.0] - 2026-04-26 - -**New**: -- Added **Services Explorer** window (`Tools > GameLovers > Services Explorer`) with 13 live-refresh tabs: Overview, Installer, MessageBroker, Tick, Coroutine, Pool, Data, Time, RNG, AssetResolver, Versioning, Assets Importer, Addressable Ids — works in both Edit and Play mode -- Menu stubs under `Tools > GameLovers`: - - `Versioning / Refresh Version Data` and `Versioning / Open in Explorer` - - `Assets Importer / Import Assets Data` and `Assets Importer / Open in Explorer` - - `Addressable Ids / Generate Addressable Ids` and `Addressable Ids / Open in Explorer` -- Added `Assets > Create > GameLovers Services > …` scaffolders: Message, Command, Service, Pool Entity (template-based, $NAME$ / $NAMESPACE$ substitution) -- Absorbed `com.gamelovers.assetsimporter` v0.5.2 into this package -- Added `IAssetLoader`, `ISceneLoader`, `AddressablesAssetLoader` to `Runtime/AssetsImporter/` (ns `GameLovers.Services.AssetsImporter`) -- Added `AddressableConfig`, `AssetConfigsScriptableObject`, `AssetLoaderUtils`, `AssetReferenceScene` to `Runtime/AssetsImporter/` (ns `GameLovers.Services.AssetsImporter`) -- Added `AssetResolverService` (implements `IAssetResolverService` / `IAssetAdderService`) to `Runtime/` root (ns `GameLovers.Services`) -- Added Editor/AssetsImporter/: `AssetsImporter`, `AssetsToolImporter`, `AssetConfigsImporter`, `AddressableIdsGenerator`, `AddressablesIdGeneratorSettings` (ns `GameLovers.Services.AssetsImporter.Editor`) -- Added importable **Samples** under `Samples~/` (importable via Unity Package Manager > GameLovers Services > Samples). - - **Services Playground** — single-scene, zero-setup walk-through that wires every foundation service via `MainInstaller` and exercises 10 of 13 Services Explorer tabs end-to-end. - - **Asset Resolver** — focused demo of `AssetResolverService` end-to-end (`AddConfigs` / `RequestAsset` / `UnloadAssets`) with `SpriteConfigs : AssetConfigsScriptableObject`. Drives the three Services Explorer tabs the Playground does not cover (Asset Resolver, Assets Importer, Addressable Ids). - -**Changed**: -- Addressable Ids generator and Assets Importer settings moved from `Assets/*.asset` ScriptableObjects to `ProjectSettings/` ScriptableSingletons (mirrors `VersioningEditorSettings`). -- Generation logic extracted from `AddressableIdsGenerator` into `AddressableIdsGeneratorUtils` (static `internal`); importer discovery/import logic extracted into `AssetsImporterEditorUtils`. -- `Tools/Assets Importer/*` and `Tools/AddressableIds Generator/*` menu entries removed. Use `Tools/GameLovers/Assets Importer/...`, `Tools/GameLovers/Addressable Ids/...`, or the Services Explorer tabs instead. -- `Toggle Auto Import On Refresh` menu entry removed. The toggle now lives exclusively in the Services Explorer **Assets Importer** tab. -- `Assets/AssetsImporter.asset` and `Assets/AddressablesIdGeneratorSettings.asset` are no longer used (settings moved to `ProjectSettings/`); safe to delete from consumer projects. -- Deleted editor source files: `AssetsImporter.cs`, `AssetsToolImporter.cs`, `AddressableIdsGenerator.cs`, `AddressablesIdGeneratorSettings.cs`. -- Folder reorganization: `Runtime/` now has domain subfolders `DependencyInjection/`, `Commands/`, `Pooling/`, `AssetsImporter/`; `Editor/` now has `Versioning/` and `AssetsImporter/` subfolders -- `Installer.cs` and `MainInstaller.cs` moved to `Runtime/DependencyInjection/` (namespace unchanged: `GameLovers.Services`) -- `CommandService.cs` trimmed to concrete class only; command contract interfaces extracted to `Runtime/Commands/` under ns `GameLovers.Services.Commands` -- `PoolService.cs` trimmed to concrete class only; pool interfaces + implementations moved to `Runtime/Pooling/` under ns `GameLovers.Services.Pooling` -- `ObjectPool.cs` (578 lines, 10 types) split into 4 files under `Runtime/Pooling/`: `IPoolEntity.cs`, `IObjectPool.cs`, `ObjectPool.cs`, `GameObjectPool.cs` -- `VersionEditorUtils.cs` and `GitEditorProcess.cs` moved to `Editor/Versioning/`; re-namespaced from `GameLovers.Services.Editor` → `GameLovers.Services.Versioning.Editor` -- Added new hard dependencies: `com.unity.addressables` (1.21.20) and `com.cysharp.unitask` (2.5.10) - -**Fixed**: -- `AddressablesAssetLoader.UnloadAsset` no longer calls `GC.Collect()`, `GC.WaitForPendingFinalizers()`, or `Resources.UnloadUnusedAssets()`. The method now only decrements the Addressables reference count. The old implementation caused PlayMode Test Runner crashes on macOS and O(total-assets-in-memory) main-thread stalls per per-asset release. Callers that need memory reclamation should invoke `Resources.UnloadUnusedAssets()` themselves at appropriate moments (scene transitions, boot, memory-pressure events); Unity also runs an unused-assets sweep automatically on `LoadSceneMode.Single` scene loads. -- Corrected `IAssetLoader.UnloadAsset` XML documentation: removed the incorrect "will also destroy GameObject instances" claim — `Addressables.Release(gameObject)` does not destroy the instance; callers must `Object.Destroy` it separately. -- `IAsyncCoroutine.StopCoroutine(bool triggerOnComplete)` now honors its `triggerOnComplete` parameter and flips `IsCompleted` to `true` and `IsRunning` to `false` after stopping. The previous implementation always invoked `OnComplete` callbacks regardless of the flag and left state flags unchanged, so consumers could not distinguish a stopped coroutine from a running one and `triggerOnComplete: false` was silently ignored. -- `GameObjectPool.Dispose()` and `GameObjectPool.Dispose()` now skip pooled entries whose underlying `GameObject` has already been destroyed by an external owner (e.g. a parent GameObject was destroyed while pooled instances were still reparented under it via `DespawnToSampleParent`). - -**Breaking Changes** — see `MIGRATION.md` for details: -- Pool types moved from `GameLovers.Services` to `GameLovers.Services.Pooling` (`IPoolService`, `IObjectPool`, `IObjectPool`, `ObjectPool`, `ObjectPoolBase`, `GameObjectPool`, `GameObjectPool`, `IPoolEntitySpawn`, `IPoolEntitySpawn`, `IPoolEntityDespawn`, `IPoolEntityObject`). `PoolService` concrete class remains in `GameLovers.Services`. -- Command contract types moved from `GameLovers.Services` to `GameLovers.Services.Commands` (`IGameCommandBase`, `IGameCommand<>`, `IGameServerCommand<>`, `ICommandService<>`). `CommandService<>` concrete class remains in `GameLovers.Services`. -- `GameLovers.AssetsImporter.*` renamed to `GameLovers.Services.AssetsImporter.*` -- `GameLovers.AssetsImporter.AssetResolverService` is now `GameLovers.Services.AssetResolverService` -- `GameLovers.Services.Editor.*` (versioning editor) renamed to `GameLovers.Services.Versioning.Editor.*` -- `IAssetLoader.UnloadAssetAsync(T, Action)` → `IAssetLoader.UnloadAsset(T, Action)`: method renamed (dropped `Async` suffix) and return type changed from `UniTask` to `void` to reflect its synchronous nature. Replace `await loader.UnloadAssetAsync(x);` with `loader.UnloadAsset(x);`. -- Code generated by `AddressableIdsGenerator` must be re-generated (updated emitted `using` statement) - -## [1.0.1] - 2026-01-14 - -**Changed**: -- Updated dependency `com.gamelovers.dataextensions` to `com.gamelovers.gamedata` -- Updated assembly definitions to reference `GameLovers.GameData` - -## [1.0.0] - 2026-01-11 - -**New**: -- Added *AGENTS.md* document to help guide AI coding assistants to understand and work with this package library -- Added an entire test suit of unit/integration/performance/smoke tests to cover all the code for all services in this package - -**Changed**: -- Changed *VersionServices* namespace from *GameLovers* to *GameLovers.Services* to maintain consistency with other services in the package. -- Made *CallOnSpawned*, *CallOnSpawned\*, and *CallOnDespawned* methods virtual in *ObjectPoolBase\* to allow derived pool classes to customize lifecycle callback behavior. - -**Fixed**: -- Fixed the *README.md* file to now follow best practices in OSS standards for Unity's package library projects -- Fixed linter warnings in *VersionServices.cs* (redundant field initialization, unused lambda parameter, member shadowing) -- Fixed *GameObjectPool* not invoking *IPoolEntitySpawn.OnSpawn()* and *IPoolEntityDespawn.OnDespawn()* on components attached to spawned GameObjects. - -## [0.15.1] - 2025-09-24 - -**New**: -- Added protected property for all private fields in *CommandService* for access to any game specific project inheritance - -## [0.15.0] - 2025-01-05 - -**New**: -- Added *StartDelayCall* method to *ICoroutineService* to allow deferred methods to be safely executed within the bounds of a Unity Coroutine -- Added the possibility to know the current state of an *IAsyncCoroutine* -- Added the access to the Sample Entity used to generete new entites within an *IObjectPool* and destroy it when disposing the object pool -- Added the possibility to reset an *IObjectPool* to a new state - -## [0.14.1] - 2024-11-30 - -**Fixed**: -- Fixed the *RngLogic.Range(float, float, bool)* method to allow having the same min and max values with maxInclusive set to true - -## [0.14.0] - 2024-11-15 - -**New**: -- Added *PublishSafe* method to *IMessageBrokerService* to allow publishing messages safely in chase of chain subscriptions during publishing of a message - -**Changed**: -- *Subscribe* and *Unsubscribe* throw an *InvalidOperationException* when being executed during a message being published - -**Fixed**: -- CoroutineTests issues running when building released projects - -## [0.13.1] - 2024-11-04 - -**Fixed**: -- Fixed the *IInstaller* when trying to bind multiple interfaces at the same time - -## [0.13.0] - 2024-11-04 - -**Changed**: -- Changed *CommandService* to now receive the *MessageBrokerService* in the command and help communication with the game architecture - -## [0.12.2] - 2024-11-02 - -**Fixed**: -- Fixed an inssue where *IPoolEntityObject.Init()* wouldn't be called when spawning entities - -## [0.12.1] - 2024-10-25 - -**Fixed**: -- The endless loop when calling *RngService.Range()* -- The endless loop *GameObjectPool* when spawning new entities - -## [0.12.0] - 2024-10-22 - -**New**: -- Added *IRngData* to *PoolService* to suppprt read only data structure and allow abtract injection of data into other objects - -**Changed**: -- Changed *RngData* to a class in orther to avoid boxing/unboxing performance when injecting *IRngData*. - -## [0.11.0] - 2024-10-19 - -**New**: -- Added *Spawn(T data)* method to *PoolService* to allow spawning new objects with defined spawning data -- Added *GetPool()* && *TryGetPool()* methods to *PoolService* to allow requesting the pool object maintained by the pool service. - -**Changed**: -- Removed *IsSpawned()* method from *PoolService* because is not a fundamental function and can now be accessed from the Pool requested from *GetPool()* -- Now *Spawn(T data)* also invokes *OnSpawn()* without data so objects that implement *IPoolEntitySpawn* have the entire behaviour lifecycle - -## [0.10.0] - 2024-10-11 - -**New**: -- Updated *CommandService* to allow non struct type commands to be executed for reference type commands -- Added *Spawn(T data)* method to pool object to allow spawning new objects with defined spawning data - -## [0.9.0] - 2024-08-10 - -**New**: -- Updated interfaces and classes related to data services, enhancing modularity and improving version handling. -- Added classes for Git commands, version management, and random number generation. - -**Changed**: -- Restructured the data service interfaces, consolidating functionality into a single *IDataService* interface and removing unnecessary interfaces. -- Changed *AddData* to *AddOrReplaceData* in the *DataService* implementation. -- Removed the *isLocal* state from data handling. - -## [0.8.1] - 2023-08-27 - -**New**: -- Added GitEditorProcess class to run Git commands as processes, enabling checks for valid Git repositories, retrieving current branch names, commit hashes, and diffs from given commits. -- Introduced *VersionEditorUtils* class for managing application versioning. This includes setting and saving the internal version before building, loading version data from disk, and generating an internal version suffix based on Git information and build settings. - -**Changed**: -- Enhanced *IInstaller* interface with new methods for binding multiple type interfaces to a single instance, improving modularity and code organization. - -## [0.8.0] - 2023-08-05 - -**New**: -- Introduced *MainInstaller*, a singleton class for managing instances in the project. -- Added *RngService* for generating and managing random numbers. -- Implemented VersionServices to manage application version, including asynchronous loading of version data and comparison of version strings. - -## [0.7.1] - 2023-07-28 - -**Changed**: -- Tests have been moved to proper folders, and the package number has been updated. -- An unused namespace import has been removed from the InstallerTest class. - -**Fixed**: -- Compilation errors in various test files and the PoolService class have been fixed. - -## [0.7.0] - 2023-07-28 - -**New**: -- Introduced a code review process using GitHub Actions workflow. -- Added *IInstaller* interface and Installer implementation for binding and resolving instances. -- Updated namespaces, removed unused code, and modified method calls in test classes. - -**Changed**: -- Removed dependency on *ICommandNetworkService *and SendCommand method in *CommandService*. -- Updated *IDataService* interface and *DataService* class to handle local and online data saving. -- Improved readability of *MessageBrokerService* class by using var for type inference. -- Removed unused network service related interfaces, classes, and methods. -- Modified calculation of overFlow in TickService to check for zero DeltaTime. - -## [0.6.2] - 2020-09-10 - -**Changed**: -- Made *NetworkService* abstract and removed *INetworkService* to make easier to work with -- Improved Readme documentation - -## [0.6.1] - 2020-09-09 - -**New**: -- Added connection between *NetworkService* & *CommandService* -- Added integration tests - -## [0.6.0] - 2020-09-09 - -**New**: -- Added *NetworkService* -- Improved Readme documentation - -## [0.5.0] - 2020-07-10 - -**Changed**: -- Renamed *IDataWriter* and it's *FlushData* methods to *IDataSaver* & *SaveData* respectively to match with it's execution logic scope -- Moved the *AddData* to the *IDataService* to allow the *IDataSaver* have the single responsibility of saving data into disk - -## [0.4.1] - 2020-07-09 - -**New**: -- Added *CommandService* - -## [0.4.0] - 2020-07-09 - -**New**: -- Added *DataService* - -## [0.3.1] - 2020-02-25 - -**Fixed**: -- Fixed object pool despawn all elements. It was not despawning all the elements -- Fixed issue preventing to stop coroutines and thrown MissingReferenceException - -## [0.3.0] - 2020-02-09 - -**Changed**: -- Now the *MainInstaller* checks the object binding relationship in compile time -- Improved the *ObjectPools* helper classes with a now static global instatiator for game objects. -- Now the *PoolService* is only a service container for objects pools and no longer creates/initializes new pools. -- Removed *Pool.Clear* functionality. Use *DespawnAll* or delete the pool instead - -**Fixed**: -- The *CoroutineService* no longer fails on null coroutines - -## [0.2.0] - 2020-01-19 - -- Added new *ObjectPool* & *GameObjectPool* pools to allow to allow to use object pools independent from the *PoolService*. This allows to have different pools of the same type in the project in different object controllers -- Added new interface *IPoolEntityClear* that allows a callback method for entities when they are cleared from the pool -- Added new unit tests for the *ObjectPool* - -**Changed**: -- Now the *PoolService.Clear()* does not take any action parameters. To have a callback when the entity is cleared, please have the entity implement the *IPoolEntityClear* interface - -## [0.1.1] - 2020-01-06 - -**New**: -- Added License - -## [0.1.0] - 2020-01-06 - -- Initial submission for package distribution +# Changelog +All notable changes to this package will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +**Fixed**: +- `GameObjectPool.Dispose(bool disposeSampleEntity)` and `GameObjectPool.Dispose(bool)` destroyed `SampleEntity` unconditionally, ignoring the argument — `Dispose(false)` destroyed a sample entity the caller explicitly asked to keep. +- `ObjectPoolBase.SpawnEntity` could hand out a destroyed pooled object. Its retry loop tested `entity == null`, which inside a generic constrained only to `class` compiles to plain reference equality and never reaches `UnityEngine.Object`'s overloaded `==` that detects a destroyed ("fake-null") native object. A pooled `GameObject`/`Behaviour` destroyed by an external owner while despawned was therefore returned to the next `Spawn()` caller, who hit a `MissingReferenceException` a frame later. Now routed through a runtime type check that dispatches to the Unity overload when the entity is a `UnityEngine.Object`, and falls back to reference equality for POCO pooled types. +- `AddressableIdsGeneratorUtils` emitted duplicate enum members when two Addressable addresses sanitized to the same C# identifier (e.g. `ui/main-menu` and `ui-main/menu` both clean to `ui_main_menu`), producing generated code that does not compile. The member-append path now uses the disambiguated name it already computed instead of re-deriving the raw cleaned name. + +**Changed**: +- `package.json` now declares `com.unity.test-framework.performance` (3.5.0). Both test asmdefs already referenced `Unity.PerformanceTesting` unconditionally, so consumers without that package installed hit a missing-assembly compile error in this package's test assemblies. + +## [2.1.2] - 2026-07-29 + +**Fixed**: +- Renamed `Tests/EditMode/GameLovers.Services.Tests.asmdef` to `GameLovers.Services.Editor.Tests.asmdef` to match its own `name` field (`GameLovers.Services.Editor.Tests`); GUID preserved via `git mv` on the paired `.meta`. +- `Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef` no longer sets `autoReferenced: true` (was the only test asmdef in the repo doing so). Test discovery is unaffected — the Test Runner finds test assemblies via the `UNITY_INCLUDE_TESTS` define constraint, independent of `autoReferenced`. + +**Docs**: +- `Samples~/ServicesPlayground`'s `package.json` description no longer claims the sample UI "is built programmatically" — it ships as a hand-authored prefab (`ServicesPlaygroundUI.prefab`) with a `[SerializeField]`-wired driver script. +- Converged `README.md`'s repository links on the actual origin. + +## [2.1.1] - 2026-07-04 + +**Changed**: +- PlayMode tests updated to parameterless `FindObjectsByType()` overload (Unity 6 API). +- Removed redundant `[Serializable]` from `AddressableConfig` (already implicitly serializable as a reference type in Unity YAML). + +**Fixed**: +- `ServicesScaffolders` adapts to Unity 6000.4+ `AssetCreationEndAction` / `EntityId` API (guarded by `UNITY_6000_4_OR_NEWER`; pre-6000.4 path unchanged). + +## [2.1.0] - 2026-05-20 + +**New**: +- `VersionServices` now auto-bootstraps via `[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]`, populating version metadata before any scene `Awake` callback and before vendor-SDK `SubsystemRegistration` callbacks that read it. Consumers no longer need to call `LoadVersionData()` / `LoadVersionDataAsync()` explicitly for the default flow. + +**Changed**: +- Property getters (`VersionInternal`, `Branch`, `Commit`, `BuildNumber`) now lazy-load via a new private `EnsureLoaded()` on first access if the auto-bootstrap hook has not yet fired — protects against undefined ordering between sibling assemblies' `[RuntimeInitializeOnLoadMethod]` callbacks at the same phase. +- Removed the private `IsLoaded()` helper (replaced by `EnsureLoaded()` invoked from each property getter). + +**Docs**: +- `docs/version-services.md` rewritten around the new auto-bootstrap contract: the recommended usage no longer includes any explicit load call, the lazy-load fallback is documented, and the Error Reference table now describes the fallback behaviour (no exception is raised). + +## [2.0.2] - 2026-05-20 + +**Fixed**: +- Add missing meta file + +## [2.0.1] - 2026-05-20 + +**New** +- Added new test suite for more rebust code coverage +- Added `VersionServices.LoadVersionData()` — synchronous sibling of `LoadVersionDataAsync()` for consumers who want to populate version metadata at boot without an `await`. Both methods now funnel into a shared private `ApplyTextAsset` helper, so behaviour is identical. Sync is the recommended default for the shipping `version-data.txt` (a few hundred bytes); async remains available for cases where `VersionData` is extended with large embedded blobs. Covered by `Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs`. + +**Fixed**: +- `Tests/AGENTS.md` §8 extended with a new **"Authorized reflection sites (storage-assertion exception)"** subsection. + +## [2.0.0] - 2026-04-26 + +**New**: +- Added **Services Explorer** window (`Tools > GameLovers > Services Explorer`) with 13 live-refresh tabs: Overview, Installer, MessageBroker, Tick, Coroutine, Pool, Data, Time, RNG, AssetResolver, Versioning, Assets Importer, Addressable Ids — works in both Edit and Play mode +- Menu stubs under `Tools > GameLovers`: + - `Versioning / Refresh Version Data` and `Versioning / Open in Explorer` + - `Assets Importer / Import Assets Data` and `Assets Importer / Open in Explorer` + - `Addressable Ids / Generate Addressable Ids` and `Addressable Ids / Open in Explorer` +- Added `Assets > Create > GameLovers Services > …` scaffolders: Message, Command, Service, Pool Entity (template-based, $NAME$ / $NAMESPACE$ substitution) +- Absorbed `com.gamelovers.assetsimporter` v0.5.2 into this package +- Added `IAssetLoader`, `ISceneLoader`, `AddressablesAssetLoader` to `Runtime/AssetsImporter/` (ns `GameLovers.Services.AssetsImporter`) +- Added `AddressableConfig`, `AssetConfigsScriptableObject`, `AssetLoaderUtils`, `AssetReferenceScene` to `Runtime/AssetsImporter/` (ns `GameLovers.Services.AssetsImporter`) +- Added `AssetResolverService` (implements `IAssetResolverService` / `IAssetAdderService`) to `Runtime/` root (ns `GameLovers.Services`) +- Added Editor/AssetsImporter/: `AssetsImporter`, `AssetsToolImporter`, `AssetConfigsImporter`, `AddressableIdsGenerator`, `AddressablesIdGeneratorSettings` (ns `GameLovers.Services.AssetsImporter.Editor`) +- Added importable **Samples** under `Samples~/` (importable via Unity Package Manager > GameLovers Services > Samples). + - **Services Playground** — single-scene, zero-setup walk-through that wires every foundation service via `MainInstaller` and exercises 10 of 13 Services Explorer tabs end-to-end. + - **Asset Resolver** — focused demo of `AssetResolverService` end-to-end (`AddConfigs` / `RequestAsset` / `UnloadAssets`) with `SpriteConfigs : AssetConfigsScriptableObject`. Drives the three Services Explorer tabs the Playground does not cover (Asset Resolver, Assets Importer, Addressable Ids). + +**Changed**: +- Addressable Ids generator and Assets Importer settings moved from `Assets/*.asset` ScriptableObjects to `ProjectSettings/` ScriptableSingletons (mirrors `VersioningEditorSettings`). +- Generation logic extracted from `AddressableIdsGenerator` into `AddressableIdsGeneratorUtils` (static `internal`); importer discovery/import logic extracted into `AssetsImporterEditorUtils`. +- `Tools/Assets Importer/*` and `Tools/AddressableIds Generator/*` menu entries removed. Use `Tools/GameLovers/Assets Importer/...`, `Tools/GameLovers/Addressable Ids/...`, or the Services Explorer tabs instead. +- `Toggle Auto Import On Refresh` menu entry removed. The toggle now lives exclusively in the Services Explorer **Assets Importer** tab. +- `Assets/AssetsImporter.asset` and `Assets/AddressablesIdGeneratorSettings.asset` are no longer used (settings moved to `ProjectSettings/`); safe to delete from consumer projects. +- Deleted editor source files: `AssetsImporter.cs`, `AssetsToolImporter.cs`, `AddressableIdsGenerator.cs`, `AddressablesIdGeneratorSettings.cs`. +- Folder reorganization: `Runtime/` now has domain subfolders `DependencyInjection/`, `Commands/`, `Pooling/`, `AssetsImporter/`; `Editor/` now has `Versioning/` and `AssetsImporter/` subfolders +- `Installer.cs` and `MainInstaller.cs` moved to `Runtime/DependencyInjection/` (namespace unchanged: `GameLovers.Services`) +- `CommandService.cs` trimmed to concrete class only; command contract interfaces extracted to `Runtime/Commands/` under ns `GameLovers.Services.Commands` +- `PoolService.cs` trimmed to concrete class only; pool interfaces + implementations moved to `Runtime/Pooling/` under ns `GameLovers.Services.Pooling` +- `ObjectPool.cs` (578 lines, 10 types) split into 4 files under `Runtime/Pooling/`: `IPoolEntity.cs`, `IObjectPool.cs`, `ObjectPool.cs`, `GameObjectPool.cs` +- `VersionEditorUtils.cs` and `GitEditorProcess.cs` moved to `Editor/Versioning/`; re-namespaced from `GameLovers.Services.Editor` → `GameLovers.Services.Versioning.Editor` +- Added new hard dependencies: `com.unity.addressables` (1.21.20) and `com.cysharp.unitask` (2.5.10) + +**Fixed**: +- `AddressablesAssetLoader.UnloadAsset` no longer calls `GC.Collect()`, `GC.WaitForPendingFinalizers()`, or `Resources.UnloadUnusedAssets()`. The method now only decrements the Addressables reference count. The old implementation caused PlayMode Test Runner crashes on macOS and O(total-assets-in-memory) main-thread stalls per per-asset release. Callers that need memory reclamation should invoke `Resources.UnloadUnusedAssets()` themselves at appropriate moments (scene transitions, boot, memory-pressure events); Unity also runs an unused-assets sweep automatically on `LoadSceneMode.Single` scene loads. +- Corrected `IAssetLoader.UnloadAsset` XML documentation: removed the incorrect "will also destroy GameObject instances" claim — `Addressables.Release(gameObject)` does not destroy the instance; callers must `Object.Destroy` it separately. +- `IAsyncCoroutine.StopCoroutine(bool triggerOnComplete)` now honors its `triggerOnComplete` parameter and flips `IsCompleted` to `true` and `IsRunning` to `false` after stopping. The previous implementation always invoked `OnComplete` callbacks regardless of the flag and left state flags unchanged, so consumers could not distinguish a stopped coroutine from a running one and `triggerOnComplete: false` was silently ignored. +- `GameObjectPool.Dispose()` and `GameObjectPool.Dispose()` now skip pooled entries whose underlying `GameObject` has already been destroyed by an external owner (e.g. a parent GameObject was destroyed while pooled instances were still reparented under it via `DespawnToSampleParent`). + +**Breaking Changes** — see `MIGRATION.md` for details: +- Pool types moved from `GameLovers.Services` to `GameLovers.Services.Pooling` (`IPoolService`, `IObjectPool`, `IObjectPool`, `ObjectPool`, `ObjectPoolBase`, `GameObjectPool`, `GameObjectPool`, `IPoolEntitySpawn`, `IPoolEntitySpawn`, `IPoolEntityDespawn`, `IPoolEntityObject`). `PoolService` concrete class remains in `GameLovers.Services`. +- Command contract types moved from `GameLovers.Services` to `GameLovers.Services.Commands` (`IGameCommandBase`, `IGameCommand<>`, `IGameServerCommand<>`, `ICommandService<>`). `CommandService<>` concrete class remains in `GameLovers.Services`. +- `GameLovers.AssetsImporter.*` renamed to `GameLovers.Services.AssetsImporter.*` +- `GameLovers.AssetsImporter.AssetResolverService` is now `GameLovers.Services.AssetResolverService` +- `GameLovers.Services.Editor.*` (versioning editor) renamed to `GameLovers.Services.Versioning.Editor.*` +- `IAssetLoader.UnloadAssetAsync(T, Action)` → `IAssetLoader.UnloadAsset(T, Action)`: method renamed (dropped `Async` suffix) and return type changed from `UniTask` to `void` to reflect its synchronous nature. Replace `await loader.UnloadAssetAsync(x);` with `loader.UnloadAsset(x);`. +- Code generated by `AddressableIdsGenerator` must be re-generated (updated emitted `using` statement) + +## [1.0.1] - 2026-01-14 + +**Changed**: +- Updated dependency `com.gamelovers.dataextensions` to `com.gamelovers.gamedata` +- Updated assembly definitions to reference `GameLovers.GameData` + +## [1.0.0] - 2026-01-11 + +**New**: +- Added *AGENTS.md* document to help guide AI coding assistants to understand and work with this package library +- Added an entire test suit of unit/integration/performance/smoke tests to cover all the code for all services in this package + +**Changed**: +- Changed *VersionServices* namespace from *GameLovers* to *GameLovers.Services* to maintain consistency with other services in the package. +- Made *CallOnSpawned*, *CallOnSpawned\*, and *CallOnDespawned* methods virtual in *ObjectPoolBase\* to allow derived pool classes to customize lifecycle callback behavior. + +**Fixed**: +- Fixed the *README.md* file to now follow best practices in OSS standards for Unity's package library projects +- Fixed linter warnings in *VersionServices.cs* (redundant field initialization, unused lambda parameter, member shadowing) +- Fixed *GameObjectPool* not invoking *IPoolEntitySpawn.OnSpawn()* and *IPoolEntityDespawn.OnDespawn()* on components attached to spawned GameObjects. + +## [0.15.1] - 2025-09-24 + +**New**: +- Added protected property for all private fields in *CommandService* for access to any game specific project inheritance + +## [0.15.0] - 2025-01-05 + +**New**: +- Added *StartDelayCall* method to *ICoroutineService* to allow deferred methods to be safely executed within the bounds of a Unity Coroutine +- Added the possibility to know the current state of an *IAsyncCoroutine* +- Added the access to the Sample Entity used to generete new entites within an *IObjectPool* and destroy it when disposing the object pool +- Added the possibility to reset an *IObjectPool* to a new state + +## [0.14.1] - 2024-11-30 + +**Fixed**: +- Fixed the *RngLogic.Range(float, float, bool)* method to allow having the same min and max values with maxInclusive set to true + +## [0.14.0] - 2024-11-15 + +**New**: +- Added *PublishSafe* method to *IMessageBrokerService* to allow publishing messages safely in chase of chain subscriptions during publishing of a message + +**Changed**: +- *Subscribe* and *Unsubscribe* throw an *InvalidOperationException* when being executed during a message being published + +**Fixed**: +- CoroutineTests issues running when building released projects + +## [0.13.1] - 2024-11-04 + +**Fixed**: +- Fixed the *IInstaller* when trying to bind multiple interfaces at the same time + +## [0.13.0] - 2024-11-04 + +**Changed**: +- Changed *CommandService* to now receive the *MessageBrokerService* in the command and help communication with the game architecture + +## [0.12.2] - 2024-11-02 + +**Fixed**: +- Fixed an inssue where *IPoolEntityObject.Init()* wouldn't be called when spawning entities + +## [0.12.1] - 2024-10-25 + +**Fixed**: +- The endless loop when calling *RngService.Range()* +- The endless loop *GameObjectPool* when spawning new entities + +## [0.12.0] - 2024-10-22 + +**New**: +- Added *IRngData* to *PoolService* to suppprt read only data structure and allow abtract injection of data into other objects + +**Changed**: +- Changed *RngData* to a class in orther to avoid boxing/unboxing performance when injecting *IRngData*. + +## [0.11.0] - 2024-10-19 + +**New**: +- Added *Spawn(T data)* method to *PoolService* to allow spawning new objects with defined spawning data +- Added *GetPool()* && *TryGetPool()* methods to *PoolService* to allow requesting the pool object maintained by the pool service. + +**Changed**: +- Removed *IsSpawned()* method from *PoolService* because is not a fundamental function and can now be accessed from the Pool requested from *GetPool()* +- Now *Spawn(T data)* also invokes *OnSpawn()* without data so objects that implement *IPoolEntitySpawn* have the entire behaviour lifecycle + +## [0.10.0] - 2024-10-11 + +**New**: +- Updated *CommandService* to allow non struct type commands to be executed for reference type commands +- Added *Spawn(T data)* method to pool object to allow spawning new objects with defined spawning data + +## [0.9.0] - 2024-08-10 + +**New**: +- Updated interfaces and classes related to data services, enhancing modularity and improving version handling. +- Added classes for Git commands, version management, and random number generation. + +**Changed**: +- Restructured the data service interfaces, consolidating functionality into a single *IDataService* interface and removing unnecessary interfaces. +- Changed *AddData* to *AddOrReplaceData* in the *DataService* implementation. +- Removed the *isLocal* state from data handling. + +## [0.8.1] - 2023-08-27 + +**New**: +- Added GitEditorProcess class to run Git commands as processes, enabling checks for valid Git repositories, retrieving current branch names, commit hashes, and diffs from given commits. +- Introduced *VersionEditorUtils* class for managing application versioning. This includes setting and saving the internal version before building, loading version data from disk, and generating an internal version suffix based on Git information and build settings. + +**Changed**: +- Enhanced *IInstaller* interface with new methods for binding multiple type interfaces to a single instance, improving modularity and code organization. + +## [0.8.0] - 2023-08-05 + +**New**: +- Introduced *MainInstaller*, a singleton class for managing instances in the project. +- Added *RngService* for generating and managing random numbers. +- Implemented VersionServices to manage application version, including asynchronous loading of version data and comparison of version strings. + +## [0.7.1] - 2023-07-28 + +**Changed**: +- Tests have been moved to proper folders, and the package number has been updated. +- An unused namespace import has been removed from the InstallerTest class. + +**Fixed**: +- Compilation errors in various test files and the PoolService class have been fixed. + +## [0.7.0] - 2023-07-28 + +**New**: +- Introduced a code review process using GitHub Actions workflow. +- Added *IInstaller* interface and Installer implementation for binding and resolving instances. +- Updated namespaces, removed unused code, and modified method calls in test classes. + +**Changed**: +- Removed dependency on *ICommandNetworkService *and SendCommand method in *CommandService*. +- Updated *IDataService* interface and *DataService* class to handle local and online data saving. +- Improved readability of *MessageBrokerService* class by using var for type inference. +- Removed unused network service related interfaces, classes, and methods. +- Modified calculation of overFlow in TickService to check for zero DeltaTime. + +## [0.6.2] - 2020-09-10 + +**Changed**: +- Made *NetworkService* abstract and removed *INetworkService* to make easier to work with +- Improved Readme documentation + +## [0.6.1] - 2020-09-09 + +**New**: +- Added connection between *NetworkService* & *CommandService* +- Added integration tests + +## [0.6.0] - 2020-09-09 + +**New**: +- Added *NetworkService* +- Improved Readme documentation + +## [0.5.0] - 2020-07-10 + +**Changed**: +- Renamed *IDataWriter* and it's *FlushData* methods to *IDataSaver* & *SaveData* respectively to match with it's execution logic scope +- Moved the *AddData* to the *IDataService* to allow the *IDataSaver* have the single responsibility of saving data into disk + +## [0.4.1] - 2020-07-09 + +**New**: +- Added *CommandService* + +## [0.4.0] - 2020-07-09 + +**New**: +- Added *DataService* + +## [0.3.1] - 2020-02-25 + +**Fixed**: +- Fixed object pool despawn all elements. It was not despawning all the elements +- Fixed issue preventing to stop coroutines and thrown MissingReferenceException + +## [0.3.0] - 2020-02-09 + +**Changed**: +- Now the *MainInstaller* checks the object binding relationship in compile time +- Improved the *ObjectPools* helper classes with a now static global instatiator for game objects. +- Now the *PoolService* is only a service container for objects pools and no longer creates/initializes new pools. +- Removed *Pool.Clear* functionality. Use *DespawnAll* or delete the pool instead + +**Fixed**: +- The *CoroutineService* no longer fails on null coroutines + +## [0.2.0] - 2020-01-19 + +- Added new *ObjectPool* & *GameObjectPool* pools to allow to allow to use object pools independent from the *PoolService*. This allows to have different pools of the same type in the project in different object controllers +- Added new interface *IPoolEntityClear* that allows a callback method for entities when they are cleared from the pool +- Added new unit tests for the *ObjectPool* + +**Changed**: +- Now the *PoolService.Clear()* does not take any action parameters. To have a callback when the entity is cleared, please have the entity implement the *IPoolEntityClear* interface + +## [0.1.1] - 2020-01-06 + +**New**: +- Added License + +## [0.1.0] - 2020-01-06 + +- Initial submission for package distribution diff --git a/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs b/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs index 174e428..e30fbd7 100644 --- a/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs +++ b/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs @@ -278,9 +278,7 @@ private static void GenerateScript(List assetList, Addres stringBuilder.AppendLine("{"); stringBuilder.AppendLine($"\tpublic enum {settings.ScriptFilename}"); - stringBuilder.AppendLine("\t{"); - GenerateAddressEnums(stringBuilder, assetList); - stringBuilder.AppendLine("\t}"); + stringBuilder.Append(BuildEnumSource(ExtractAddresses(assetList))); stringBuilder.AppendLine(""); stringBuilder.AppendLine("\tpublic enum AddressableLabel"); @@ -485,26 +483,48 @@ private static void ProcessData(IList assetList, Addressa } } - private static void GenerateAddressEnums(StringBuilder stringBuilder, IReadOnlyList assetList) + /// + /// Pure string-builder: turns into the generated C# enum member-block + /// source text (opening brace, one member per address, closing brace) that + /// inserts after the public enum <Name> header line. Takes no AssetDatabase + /// dependency — operates on plain address strings only, so it is directly unit-testable. + /// Extracted from the previous inlined GenerateAddressEnums(StringBuilder, IReadOnlyList<AddressableAssetEntry>); + /// no behaviour change versus the logic it replaces. + /// + internal static string BuildEnumSource(IReadOnlyCollection addresses) + { + var addressList = addresses as IReadOnlyList ?? new List(addresses); + var stringBuilder = new StringBuilder(); + + stringBuilder.AppendLine("\t{"); + AppendAddressEnumMembers(stringBuilder, addressList); + stringBuilder.AppendLine("\t}"); + + return stringBuilder.ToString(); + } + + private static void AppendAddressEnumMembers(StringBuilder stringBuilder, IReadOnlyList addresses) { var addedNames = new List(); - for (var i = 0; i < assetList.Count; i++) + for (var i = 0; i < addresses.Count; i++) { - var name = ResolveSanitizedEnumName(assetList[i].address, addedNames, out _); + var name = ResolveSanitizedEnumName(addresses[i], addedNames, out _); addedNames.Add(name); stringBuilder.Append("\t\t"); - stringBuilder.Append(GetCleanName(assetList[i].address, true)); - stringBuilder.Append(i + 1 == assetList.Count ? "\n" : ",\n"); + stringBuilder.Append(name); + stringBuilder.Append(i + 1 == addresses.Count ? "\n" : ",\n"); } } /// - /// Resolves the enum-member name for a given Addressable , applying the - /// same name_filetype fallback that uses when the cleaned - /// name collides with one already in . Sets - /// to true when the fallback path was taken. + /// Resolves the name_filetype-suffixed disambiguation candidate for a given Addressable + /// when its cleaned name collides with one already in + /// . Sets to true when the fallback + /// path was taken. Used by both (to emit a unique enum member + /// name) and (to report collisions for the Explorer diff + /// view). /// private static string ResolveSanitizedEnumName(string address, List seenNames, out bool collided) { diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs index 1e885dc..333f793 100644 --- a/Runtime/AssemblyInfo.cs +++ b/Runtime/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("GameLovers.Services.Editor")] +[assembly: InternalsVisibleTo("GameLovers.Services.Editor.Tests")] diff --git a/Runtime/AssetResolverService.cs b/Runtime/AssetResolverService.cs index 4adbf65..72cab75 100644 --- a/Runtime/AssetResolverService.cs +++ b/Runtime/AssetResolverService.cs @@ -416,72 +416,85 @@ public void AddDebugConfigs(Sprite errorSprite = null, GameObject errorCube = nu private TAsset Convert(AssetReference assetReference, bool instantiate) where TAsset : Object { - var type = typeof(TAsset); - - /* AssetReference types - - GameObject - ScriptableObject - Texture - Texture3D - Texture2D - RenderTexture - CustomRenderTexture - CubeMap - Material - PhysicMaterial - PhysicMaterial2D - Sprite - SpriteAtlas - VideoClip - AudioClip - AudioMixer - Avatar - AnimatorController - AnimatorOverrideController - TextAsset - Mesh - Shader - ComputeShader - Flare - NavMeshData - TerrainData - TerrainLayer - Font - Scene - GUISkin - * */ - - if (assetReference.Asset == null) + return SelectAsset(typeof(TAsset), assetReference.Asset, assetReference.IsDone, instantiate, + _errorSprite, _errorCube, _errorMaterial, _errorClip); + } + + /// + /// Pure type-switch that resolves the to return for a given Addressables + /// / pair, substituting the matching error placeholder + /// (///) + /// when the reference has not finished loading. Extracted from for direct + /// testability; no behaviour change versus the inlined switch it replaces. + /// + /* + * AssetReference types + + GameObject + ScriptableObject + Texture + Texture3D + Texture2D + RenderTexture + CustomRenderTexture + CubeMap + Material + PhysicMaterial + PhysicMaterial2D + Sprite + SpriteAtlas + VideoClip + AudioClip + AudioMixer + Avatar + AnimatorController + AnimatorOverrideController + TextAsset + Mesh + Shader + ComputeShader + Flare + NavMeshData + TerrainData + TerrainLayer + Font + Scene + GUISkin + * */ + internal static TAsset SelectAsset(Type type, Object asset, bool isDone, bool instantiate, + Sprite errorSprite, GameObject errorCube, Material errorMaterial, AudioClip errorClip) + where TAsset : Object + { + if (asset == null) { return null; } if (type == typeof(GameObject)) { - var asset = !assetReference.IsDone ? _errorCube : assetReference.Asset as GameObject; + var selected = !isDone ? errorCube : asset as GameObject; - return instantiate ? Object.Instantiate(asset) as TAsset : asset as TAsset; + return instantiate ? Object.Instantiate(selected) as TAsset : selected as TAsset; } if (type == typeof(Sprite)) { - return !assetReference.IsDone ? _errorSprite as TAsset : assetReference.Asset as TAsset; + return !isDone ? errorSprite as TAsset : asset as TAsset; } if (type == typeof(Material)) { - var asset = !assetReference.IsDone ? _errorMaterial : assetReference.Asset as Material; + var selected = !isDone ? errorMaterial : asset as Material; - return instantiate ? new Material(asset) as TAsset : asset as TAsset; + return instantiate ? new Material(selected) as TAsset : selected as TAsset; } if (type == typeof(AudioClip)) { - return !assetReference.IsDone ? _errorClip as TAsset : assetReference.Asset as TAsset; + return !isDone ? errorClip as TAsset : asset as TAsset; } - return assetReference.Asset as TAsset; + return asset as TAsset; } private bool TryGetDictionary(out Dictionary dictionary) diff --git a/Runtime/Pooling/GameObjectPool.cs b/Runtime/Pooling/GameObjectPool.cs index cac3fcf..2b573bb 100644 --- a/Runtime/Pooling/GameObjectPool.cs +++ b/Runtime/Pooling/GameObjectPool.cs @@ -29,7 +29,10 @@ public GameObjectPool(uint initSize, GameObject sampleEntity, Func public override void Dispose(bool disposeSampleEntity) { - Object.Destroy(SampleEntity); + if (disposeSampleEntity) + { + Object.Destroy(SampleEntity); + } base.Dispose(disposeSampleEntity); } @@ -130,7 +133,10 @@ public GameObjectPool(uint initSize, T sampleEntity, Func instantiator) : /// public override void Dispose(bool disposeSampleEntity) { - Object.Destroy(SampleEntity.gameObject); + if (disposeSampleEntity) + { + Object.Destroy(SampleEntity.gameObject); + } base.Dispose(disposeSampleEntity); } diff --git a/Runtime/Pooling/ObjectPool.cs b/Runtime/Pooling/ObjectPool.cs index 5f5016c..7dd3b36 100644 --- a/Runtime/Pooling/ObjectPool.cs +++ b/Runtime/Pooling/ObjectPool.cs @@ -173,7 +173,7 @@ protected virtual T SpawnEntity() } // Need to do while loop and check as parent objects could have destroyed the entity/gameobject before it could // be properly disposed by pool service - while (entity == null); + while (IsDestroyedOrNull(entity)); SpawnedEntities.Add(entity); @@ -212,6 +212,20 @@ protected virtual void CallOnDespawned(T entity) poolEntity?.OnDespawn(); } + + /// + /// A plain entity == null inside this generic class only ever performs C# reference-equality: + /// is constrained to class, not to UnityEngine.Object, so the + /// compiler cannot dispatch to UnityEngine.Object's overloaded == that detects a + /// destroyed-but-not-null ("fake-null") native object. When 's runtime type + /// IS a UnityEngine.Object (e.g. a pooled GameObject/Behaviour), this dispatches to + /// that overload via a runtime type check instead; for a non-Unity (a POCO + /// pooled type), it falls back to a plain reference-null check. + /// + private static bool IsDestroyedOrNull(T entity) + { + return entity is UnityEngine.Object unityObject ? unityObject == null : entity == null; + } } /// diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index cc67b45..120c37e 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -1,32 +1,132 @@ -# GameLovers.Services Tests - AI Agent Guide +# GameLovers.Services Tests — AI Agent Guide This file contains testing conventions for the `com.gamelovers.services` package. It is the source of truth when reading, editing, or creating test files under `Tests/`. For runtime architecture, gotchas, and package-level context, see the parent [`AGENTS.md`](../AGENTS.md). -## 1. Placement Rules (EditMode vs PlayMode) +§1 and §2 are shared verbatim across every GameLovers package. A change to either must be applied to all six `Tests/AGENTS.md` files in the same working session, one commit per submodule. + +## 1. ADMIT — Test Admission Test + +A proposed test is admitted only if all five answers are YES. Record the first two +as comments on the test itself. + +| | Question | +|---|---| +| **A1 DEFECT** | Can you name the defect in one sentence, referencing a production file and symbol? "It could break" is not a defect. | +| **A2 RED** | Can you name the exact production edit — one line or one branch, identified by `file` + `symbol` — that makes this test fail? If no such single edit exists, the test pins nothing. | +| **A3 PACKAGE** | Does every assertion read a value this package computed? Reject assertions on `new X() != new X()`, `!= null` on a freshly constructed object, default struct/enum values, or anything the C# spec or the Unity engine already guarantees. | +| **A4 CHEAPEST** | Is this the cheapest tier that covers the defect? EditMode beats PlayMode; a `[TestCase]` row on an existing fixture beats a new `[Test]`; a new `[Test]` beats a new fixture. Grep before writing. | +| **A5 UNIQUE** | Does no existing test already fail on the A2 edit? Grep the symbol under test across `Tests/` first. | + +**A5-bis — inherited-type coverage.** Before proposing a fixture for a type that +derives from or wraps another tested type, grep `Tests/` for the derived type's +name and for paired `[SetUp]` fields. Base-and-derived pairs are tested jointly in +the base's fixture unless the derived type adds new public surface. + +**Two mechanical disqualifiers** — violate one and the test is rejected: + +- **D1 — tautology.** If the only assertion is `Assert.DoesNotThrow`, + `Assert.IsNotNull`, or a disjunction of `Contains(...)` substrings, the test + fails A2 unless you write down what *would* throw, be null, or not match. A + substring disjunction that includes a string the input itself embeds is + unfalsifiable by construction. +- **D2 — name/body contract.** The test name is a claim. If deleting the + production feature the name mentions leaves the test green, the name is a lie. + +**Smoke exemption, by directory.** Fixtures under `Smoke/` are exempt from A1 and +A2 and may assert construction-without-throwing only. Their defect class is "the +assembly no longer loads / bootstrap regressed", which is real and not expressible +otherwise. The exemption is by directory, not by assertion shape — a Unit test +that only asserts `IsNotNull` is still rejected. + +## 2. RCR — Revert and Confirm Red + +> Every new or strengthened test must be observed failing, once, against a +> one-line production revert, before it is committed. + +Line coverage proves a line executed. It does not prove any test would notice if +that line were wrong. RCR is the cheap substitute for mutation testing, and it is +what makes a coverage number trustworthy. + +**Procedure** (~90 seconds per test): + +1. Write the test. Run it. Green. +2. Apply the A2 edit — invert the comparison, delete the guard clause, return + early, comment out the one line. **One line only**: a broad deletion proves + nothing, because it would also "fail" a tautological test via a compile error. +3. Run only that test. It must be **RED**, and the failure message must name the + thing you broke. A red-by-`NullReferenceException` does not count — that is the + test crashing, not asserting. +4. `git checkout -- `. Re-run. Green. +5. Record the mutation in the test's header comment. + +**Recording format** — on the test, not in a separate ledger. A ledger rots the +moment a test is renamed; a comment travels with the test, appears in every diff +that touches it, and lets a reviewer re-run the mutation in 30 seconds. + +```csharp +[Test] +// ADMIT: +// RCR: → RED (). +public void Method_Condition_ExpectedResult() +``` + +**Anchor on `file` + `symbol`, never `file:line`.** Line numbers rot on the first +unrelated edit above them — a stale `:474` pointing at a method that moved to `:464` +sends the next reader to the wrong code and quietly destroys the comment's value. + +**Budget: four lines is the target, six is the ceiling.** One sentence of ADMIT, +one of RCR, wrapped. This obeys the repo-wide rule in the root `AGENTS.md` +(§ Code comments): *"One sentence usually suffices. Multi-paragraph rationale is a +smell."* Anything past the ceiling belongs in the commit body or `docs/`, not on the +test. Two things in particular must NOT appear here: +- **Change narration.** *"An earlier version of this test was a tautology"* is diff + context; the root `AGENTS.md` forbids it outright. A comment states the code's + permanent condition, not its history. Put it in the commit message. +- **Investigation transcript.** The empirical detail that convinced *you* is not + what the next reader needs. They need the mutation and the expected failure. + +The one extension worth its lines is a **negative** result: naming a nearby edit +that looks like a valid mutation but is NOT one (because it is already guarded, or +because it reddens a sibling test instead). That stops the next reader repeating a +dead end, and it cannot be recovered from the code. + +Also add one line per new test to the commit body: `RCR: `. +That makes `git log --grep=RCR` the audit surface. + +**Two consequences, stated so RCR does not become theatre:** + +- A test with no `// RCR:` line is not trusted coverage. In an audit it is a + suspect by default. +- **Benchmarks are included, inverted:** a performance test must be observed + *changing its number* when the measured operation is removed from the measured + body. A benchmark whose measured region does not contain the workload is a + tautology in `Measure` clothing. + +## 3. Placement Rules (EditMode vs PlayMode) - **EditMode / Unit** (`EditMode/Unit/`): Pure-logic services with no `MonoBehaviour` or `GameObject` dependency. Use `[Test]`. NSubstitute is available (referenced only in the EditMode asmdef). - **EditMode / Performance** (`EditMode/Performance/`): Perf benchmarks that do not need a running player. Require `PerformanceTestSetup` (see below). - **PlayMode / Unit** (`PlayMode/Unit/`): Services that create `DontDestroyOnLoad` GameObjects (`TickService`, `CoroutineService`, `GameObjectPool`, `GameObjectPool`). Use `[UnityTest]` returning `IEnumerator`. -- **PlayMode / Integration** (`PlayMode/Integration/`): Cross-service or async workflows (e.g., `VersionServicesIntegrationTest` loads resources). +- **PlayMode / Integration** (`PlayMode/Integration/`): Cross-service or async workflows that span multiple bound services or exercise a real load path (e.g., full service bootstrap/teardown sequences, async resource loading). - **PlayMode / Performance** (`PlayMode/Performance/`): Perf benchmarks that need a running player. - **PlayMode / Smoke** (`PlayMode/Smoke/`): Lightweight "construct without throwing" tests that confirm services instantiate and basic bind/resolve works. **Decision tree**: if the service under test creates a `GameObject` or relies on Unity callbacks → **PlayMode**; otherwise → **EditMode**. -## 2. Namespace and Suppression +## 4. Namespace and Suppression All test files use `namespace GameLoversEditor.Services.Tests` with the suppression comment: ```csharp // ReSharper disable once CheckNamespace ``` -## 3. Naming +## 5. Naming - **Test class**: `{ServiceName}Test` (e.g., `ObjectPoolTest`, `TickServiceTest`). Performance tests use `{ServiceName}PerformanceTest`. Integration tests use `{ServiceName}IntegrationTest`. - **Test method**: `MethodOrBehavior_Condition_ExpectedResult` — e.g., `Spawn_Successfully`, `Range_MinEqualsMax_ReturnsMin`, `Despawn_NotSpawnedObject_ReturnsFalse`. - **SetUp method**: Named `Init()`. - **TearDown method**: Named `Dispose()` (when calling `service.Dispose()`) or `Cleanup()` (when doing `Object.Destroy` / `MainInstaller.Clean()`). -## 4. Mock / Helper Types +## 6. Mock / Helper Types - Define mock interfaces and classes as **nested types** inside the test class (e.g., `IMockEntity`, `MockEntity`, `MockBehaviour`, `IMockSubscriber`). - EditMode tests use **NSubstitute** (`Substitute.For()`) for interface mocking. PlayMode tests use concrete `MonoBehaviour` stubs with manual counters (NSubstitute is not referenced in the PlayMode asmdef). @@ -38,16 +138,35 @@ When a test would otherwise substitute such an interface, do ONE of: - Hand-write a minimal fake class implementing the interface. Do not "work around" the proxy failure by restructuring the type hierarchy — `IMockEntity : IPoolEntityObject` is a legitimate modelling choice that the runtime code relies on. -## 5. Fields and Setup +## 7. Black-Box / Reflection Policy + +### Authorized reflection sites (storage-assertion exception) +Reflection on private state is also authorized when a setter has no observable readback path through the public API and exercising the side-effect would require a runtime environment the EditMode harness cannot provide (e.g., a partially-loaded `AssetReference`). In those cases, asserting the storage field directly via `BindingFlags.NonPublic | BindingFlags.Instance` is acceptable and preferable to a Red-testability skip. The test method MUST be a single setter-storage assertion (not a multi-step behavioural assertion); if behaviour is what you need to verify, refactor to expose an `internal` accessor under `InternalsVisibleTo` instead. + +Currently authorized: +- `AssetResolverServiceTest.AddDebugConfigs_StoresAllProvided` — reads the private `AssetResolverService._errorMaterial` field to confirm `AddDebugConfigs` stored its argument. The fallback-material lookup path (`AssetResolverService.SelectAsset` in `Runtime/AssetResolverService.cs`, called by the private `Convert` wrapper) only fires when `!assetReference.IsDone`, which the EditMode harness cannot fabricate without a real Addressables catalog. Documented here per the Type B audit run on 2026-05-04 (Referee §4 missed-anti-pattern finding, parent picked option A). + +## 8. Fields and Setup - Fields are prefixed with `_` and use **concrete service types** (not interfaces): `private TickService _tickService;`, `private ObjectPool _pool;`. - Constants use `PascalCase`: `private const int Seed = 12345;`. - `[SetUp]` creates fresh service instances. Services that create GameObjects (`TickService`, `CoroutineService`) **must** call `Dispose()` in `[TearDown]`; `GameObjectPool` tests also `Object.Destroy` the sample GameObject. +- Use `[Order(n)]` when tests must run in sequence (e.g., `VersionServicesIntegrationTest` resets static state, then loads, then reads). +- Reset shared static state in `[SetUp]` (reflection into private fields is acceptable for static classes like `VersionServices`). + +## 9. Assertion Style +- NUnit classic model is the **default**: `Assert.AreEqual`, `Assert.AreSame`, `Assert.IsTrue`, `Assert.Throws`, `Assert.DoesNotThrow`, etc. +- `Assert.That(...)` (constraint model) is permitted **ONLY** for tolerance/range constraints that classic asserts cannot express (`Is.EqualTo(x).Within(t)`, `Is.LessThan(x)`, and similar). The only authorized sites are `TimeServiceTest.cs:67,83`. Any other use is a review reject. + +## 10. PlayMode Test Cleanup -## 6. Assertion Style -- NUnit classic model only: `Assert.AreEqual`, `Assert.AreSame`, `Assert.IsTrue`, `Assert.Throws`, `Assert.DoesNotThrow`, etc. -- No constraint-model (`Assert.That(...)`) usage in the existing suite. +This package's `PlayMode/Unit/` and `PlayMode/Integration/` fixtures create `DontDestroyOnLoad` GameObjects (`TickService`, `CoroutineService`, `GameObjectPool`, `GameObjectPool`) and bind services through `MainInstaller`. These survive scene teardown between tests unless explicitly torn down, and will leak into the next test's domain if left alive. -## 7. Performance Tests +- `[TearDown]` **must** call `Dispose()` on any `TickService` / `CoroutineService` instance created in `[SetUp]` — this destroys the host `DontDestroyOnLoad` GameObject. +- `GameObjectPool` / `GameObjectPool` tests must additionally `Object.Destroy` the sample GameObject (or call `Dispose(disposeSampleEntity: true)`) so no pooled instances survive into the next test. +- Fixtures that bind through `MainInstaller.Bind(...)` (e.g. `ServiceLifecycleTest`, `VersionServicesIntegrationTest`) **must** call `MainInstaller.Clean()` in `[TearDown]` to clear bindings; a missing `Clean()` call causes the next fixture's `Bind` call to throw (`Installer` re-bind throws via `Dictionary.Add`). +- Do not rely on domain reload or Unity's own scene-unload to clean these up between tests — the Unity Test Runner does not guarantee a domain reload between every test in a fixture. + +## 11. Performance Tests - Annotate with `[Test, Performance]` and `[Category("Performance")]`. - Apply `[PrebuildSetup(typeof(PerformanceTestSetup))]` at the class level and call `PerformanceTestSetup.InitializePerformanceTestMetadata()` in `[OneTimeSetUp]`. - Use `Measure.Method(() => { ... }).WarmupCount(n).MeasurementCount(n).Run()`. @@ -63,17 +182,7 @@ Why both keys: `RunSettings.Instance` is a lazy-loaded singleton (`ResourcesLoad `PerformanceTestSetupTest.MeasureMethod_AfterInitialize_DoesNotThrow` is the regression sentinel for this contract: a no-op `Measure.Method(() => {}).WarmupCount(1).MeasurementCount(1).Run()` wrapped in `Assert.DoesNotThrow`. If a future change to `PerformanceTestSetup` drops either PlayerPref, this test fails first with a class name that points directly at the harness — keep it green. -## 8. Integration Tests -- Use `[Order(n)]` when tests must run in sequence (e.g., `VersionServicesIntegrationTest` resets static state, then loads, then reads). -- Reset shared static state in `[SetUp]` (reflection into private fields is acceptable for static classes like `VersionServices`). - -### Authorized reflection sites (storage-assertion exception) -Reflection on private state is also authorized when a setter has no observable readback path through the public API and exercising the side-effect would require a runtime environment the EditMode harness cannot provide (e.g., a partially-loaded `AssetReference`). In those cases, asserting the storage field directly via `BindingFlags.NonPublic | BindingFlags.Instance` is acceptable and preferable to a Red-testability skip. The test method MUST be a single setter-storage assertion (not a multi-step behavioural assertion); if behaviour is what you need to verify, refactor to expose an `internal` accessor under `InternalsVisibleTo` instead. - -Currently authorized: -- `AssetResolverServiceTest.AddDebugConfigs_StoresAllProvided` — reads the private `AssetResolverService._errorMaterial` field to confirm `AddDebugConfigs` stored its argument. The fallback-material lookup path (`AssetResolverService.Convert` at `Runtime/AssetResolverService.cs:474`) only fires when `!assetReference.IsDone`, which the EditMode harness cannot fabricate without a real Addressables catalog. Documented here per the Type B audit run on 2026-05-04 (Referee §4 missed-anti-pattern finding, parent picked option A). - -## 9. Test Directory Layout +## 12. Test Directory Layout | Directory | Contents | |-----------|----------| @@ -84,10 +193,39 @@ Currently authorized: | `PlayMode/Performance/` | TickService, GameObjectPool perf | | `PlayMode/Smoke/` | `ServicesBootstrapSmokeTest` | -### Note on `AddressablesAssetLoader` coverage -`AddressablesAssetLoader` is intentionally not covered by automated integration tests. It is a thin wrapper over `UnityEngine.AddressableAssets.Addressables` static APIs with no branching logic — every method is `LoadAssetAsync → ToUniTask → throw-on-failure → return`. Live integration would require a pre-built Addressables catalog plus a manually registered asset in the host project, and would validate Unity code rather than package code. The consumer layer (`AssetResolverService`) has full unit coverage via `AssetResolverServiceTest`, and the wrapper's behaviour is documented in `docs/asset-loading.md`. +## 13. Coverage Register + +Every untested symbol worth naming is either ACCEPTED (justified — do not +re-report) or OPEN (a real gap, owed a test). An untested symbol in neither state +is an audit finding. + +An ACCEPTED row needs one of exactly three falsifiable reasons: +- **(i) no branching** — zero conditionals, so there is no behaviour to pin. +- **(ii) engine-owned** — the assertion would target Unity/OS behaviour + (`[DllImport]`, `AndroidJavaObject`, Addressables statics). +- **(iii) harness-impossible** — the state cannot be fabricated in EditMode or + PlayMode, **with the specific blocker named**. + +"Low value", "hard to test", and "covered by manual QA" are NOT valid reasons. If +none of the three applies, the row is OPEN. + +ACCEPTED is dated and **expires on edit**: if the symbol's file changes, the +reason is re-checked in that PR. A `(i) no branching` row is void the moment +someone adds an `if`. + +OPEN is the only place a deletion may park coverage. A test removed for weakness +either had a stronger sibling (named in the commit body) or leaves an OPEN row. +The count of OPEN rows is the honest coverage-debt number. + +| Symbol (file:line) | State | Reason / Owed | Recorded | +|---|---|---|---| +| `AddressablesAssetLoader` (`Runtime/AssetsImporter/AddressablesAssetLoader.cs`) | ACCEPTED | (i) no branching — thin wrapper over `UnityEngine.AddressableAssets.Addressables` static APIs with no branching logic: every method is `LoadAssetAsync → ToUniTask → throw-on-failure → return`. Live integration would require a pre-built Addressables catalog plus a manually registered asset in the host project, and would validate Unity code rather than package code. The consumer layer (`AssetResolverService`) has full unit coverage via `AssetResolverServiceTest`, and the wrapper's behaviour is documented in `docs/asset-loading.md`. | 2026-07-31 | +| 25 public `Editor/` types + `ServicesScaffolders` (`Editor/**`) | ACCEPTED | (iii) harness-impossible — blocker: require `AssetDatabase` access, validated manually. Already stated in the package root [`AGENTS.md`](../AGENTS.md) §4 ("AssetsConfigsImporter (Editor)"); cross-referenced here rather than restated. | 2026-07-31 | +| `ServicesScaffolders` `#if UNITY_6000_4_OR_NEWER` guard (`Editor/Scaffolders/ServicesScaffolders.cs`) | ACCEPTED | (iii) harness-impossible — blocker: compile-time branch, only one side is reachable per Unity version, so a single test run can only ever exercise one branch of the `#if`. | 2026-07-31 | +| `VersionServices.IsOutdatedVersion` (`Runtime/VersionServices.cs`) | OPEN | Owed: only coverage was a local reimplementation of the algorithm in `VersionServicesTest.cs`; a real test against the actual method is owed. | 2026-07-31 | +| `AddressableIdsGeneratorUtils.ResolveSanitizedEnumName` 3-way collision (`Editor/AddressableIds/AddressableIdsGeneratorUtils.cs:531-539`) | OPEN | The two-address collision was fixed 2026-08-01 (`AppendAddressEnumMembers` now emits the disambiguated `name`). A deeper edge case remains: a THIRD address colliding with the same base name AND filetype re-derives the identical `"{name}_{filetype}"` suffix (the fallback only ever adds one suffix level and the collision check is against the original `name`, not against previously-suffixed candidates), so 3+ colliding addresses can still emit duplicates. Owed: either a numeric fallback (`_2`, `_3`, ...) or checking collision against the full history of emitted names, not just base names. | 2026-08-01 | -## 10. Update Policy +## 14. Update Policy Update this file when: - Test conventions change (new asmdef references, assertion style, naming patterns, new test categories) - New test directories or categories are added diff --git a/Tests/CLAUDE.md b/Tests/CLAUDE.md new file mode 100644 index 0000000..b80da31 --- /dev/null +++ b/Tests/CLAUDE.md @@ -0,0 +1,12 @@ +# Claude Code Guide — GameLovers Services Tests + +This folder's testing conventions live in `AGENTS.md`. +Claude Code will automatically import it below. + +@AGENTS.md + +## Claude-Specific Notes + +- Treat `AGENTS.md` as the source of truth. +- If anything in this file appears to conflict with `AGENTS.md`, prefer `AGENTS.md`. +- For package-level architecture and runtime gotchas, see `../AGENTS.md`. diff --git a/Tests/CLAUDE.md.meta b/Tests/CLAUDE.md.meta new file mode 100644 index 0000000..9754258 --- /dev/null +++ b/Tests/CLAUDE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c7042d6858f14cae9edf2b39b299a529 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/EditMode/Unit/AddressableIdsGeneratorTest.cs b/Tests/EditMode/Unit/AddressableIdsGeneratorTest.cs new file mode 100644 index 0000000..48b52e3 --- /dev/null +++ b/Tests/EditMode/Unit/AddressableIdsGeneratorTest.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using NUnit.Framework; +using GameLovers.Services.AddressableIds.Editor; + +// ReSharper disable once CheckNamespace + +namespace GameLoversEditor.Services.Tests +{ + [TestFixture] + public class AddressableIdsGeneratorTest + { + [Test] + // ADMIT: AddressableIdsGeneratorUtils.AppendAddressEnumMembers must append the disambiguated `name` from + // ResolveSanitizedEnumName rather than re-deriving GetCleanName(address) — two addresses that sanitize to + // the same identifier would otherwise emit duplicate enum members, a C# compile error. + // RCR: AddressableIdsGeneratorUtils.cs AppendAddressEnumMembers — revert to + // `stringBuilder.Append(GetCleanName(addresses[i], true));` → RED (both members collapse to + // "ui_main_menu"). 2026-08-01 + public void GenerateEnumSource_TwoAddressesCollideAfterSanitization_ProducesDistinctMemberNames() + { + // This pair does not collide on GetCleanName's stripped-extension rule ("ui.main.menu" would clean + // to "ui.main", not "ui_main_menu") — these two addresses differ only by which invalid-identifier + // separator they use ('-' vs '/') and both genuinely clean to "ui_main_menu", exercising the + // disambiguation fallback in ResolveSanitizedEnumName. + var addresses = new List { "ui/main-menu", "ui-main/menu" }; + + var source = AddressableIdsGeneratorUtils.BuildEnumSource(addresses); + var members = ParseMembers(source); + + Assert.AreEqual(2, members.Count); + Assert.AreEqual("ui_main_menu", members[0]); + Assert.AreNotEqual(members[0], members[1]); // the second collides, so it must take the "_" suffix + Assert.AreEqual("ui_main_menu_", members[1]); // neither address has a file extension, so filetype is empty + } + + [Test] + // ADMIT: AddressableIdsGeneratorUtils.AppendAddressEnumMembers iterates in input order with no reorder or + // dedup step, so appending a new address must not renumber existing members' implicit enum positions. + // RCR: AddressableIdsGeneratorUtils.cs AppendAddressEnumMembers — reverse the loop to + // `for (var i = addresses.Count - 1; i >= 0; i--)` → RED ("a" is no longer the first member). 2026-08-01 + public void GenerateEnumSource_AddressAppendedToInput_DoesNotRenumberExistingMembers() + { + var firstCall = AddressableIdsGeneratorUtils.BuildEnumSource(new List { "a", "b" }); + var secondCall = AddressableIdsGeneratorUtils.BuildEnumSource(new List { "a", "b", "c" }); + + var firstMembers = ParseMembers(firstCall); + var secondMembers = ParseMembers(secondCall); + + Assert.AreEqual(2, firstMembers.Count); + Assert.AreEqual(3, secondMembers.Count); + + // Position 0 and 1 carry the implicit enum values 0 and 1 (no explicit "= N" is emitted by the + // generator); appending "c" must not shift "a"/"b" off their original positions. + Assert.AreEqual(firstMembers[0], secondMembers[0]); + Assert.AreEqual(firstMembers[1], secondMembers[1]); + Assert.AreEqual("a", secondMembers[0]); + Assert.AreEqual("b", secondMembers[1]); + } + + [Test] + // ADMIT: AddressableIdsGeneratorUtils.BuildEnumSource writes both enum-body braces unconditionally, so a + // project with zero matching addresses still generates a syntactically valid empty enum. + // RCR: AddressableIdsGeneratorUtils.cs BuildEnumSource — delete `stringBuilder.AppendLine("\t}");` → RED + // (the result contains no closing brace). 2026-08-01 + public void GenerateEnumSource_EmptyAddressList_ProducesCompilableEmptyEnum() + { + var source = AddressableIdsGeneratorUtils.BuildEnumSource(new List()); + + Assert.IsTrue(source.Contains("{")); + Assert.IsTrue(source.Contains("}")); + Assert.IsFalse(source.Contains(",")); + Assert.AreEqual(0, ParseMembers(source).Count); + } + + /// + /// Splits a result into its member-name + /// tokens, in declaration order, stripping the brace lines and trailing commas. + /// + private static List ParseMembers(string enumSource) + { + var members = new List(); + var lines = enumSource.Split('\n'); + + foreach (var rawLine in lines) + { + var line = rawLine.Trim().TrimEnd(',').Trim(); + + if (line.Length == 0 || line == "{" || line == "}") + { + continue; + } + + members.Add(line); + } + + return members; + } + } +} diff --git a/Tests/EditMode/Unit/AddressableIdsGeneratorTest.cs.meta b/Tests/EditMode/Unit/AddressableIdsGeneratorTest.cs.meta new file mode 100644 index 0000000..a6670a2 --- /dev/null +++ b/Tests/EditMode/Unit/AddressableIdsGeneratorTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: eb0454030d4a84b44b4ca0fe82a86ada \ No newline at end of file diff --git a/Tests/EditMode/Unit/AssetResolverServiceTest.cs b/Tests/EditMode/Unit/AssetResolverServiceTest.cs index d99988a..cdf7a11 100644 --- a/Tests/EditMode/Unit/AssetResolverServiceTest.cs +++ b/Tests/EditMode/Unit/AssetResolverServiceTest.cs @@ -71,6 +71,28 @@ public void UnloadAssets_ClearReferences_RemovesMap() _service.UnloadAssets(clearReferences: false); } + [Test] + // ADMIT: AssetResolverService.SelectAsset must return the configured error placeholder, not the real + // not-yet-loaded reference, when `!isDone` — the package's principal silent-failure mode. + // RCR: AssetResolverService.cs SelectAsset — invert the Sprite branch to + // `isDone ? errorSprite as TAsset : asset as TAsset` → RED (returns the real Sprite). 2026-08-01 + public void SelectAsset_WhenReferenceNotDone_ReturnsErrorPlaceholderNotNull() + { + var texture = Texture2D.blackTexture; + var realSprite = Sprite.Create(texture, new Rect(0, 0, 1, 1), Vector2.zero); + var errorSprite = Sprite.Create(texture, new Rect(0, 0, 1, 1), Vector2.zero); + + var result = AssetResolverService.SelectAsset(typeof(Sprite), realSprite, isDone: false, + instantiate: false, errorSprite: errorSprite, errorCube: null, errorMaterial: null, errorClip: null); + + Assert.IsNotNull(result); + Assert.AreSame(errorSprite, result); + Assert.AreNotSame(realSprite, result); + + UnityEngine.Object.DestroyImmediate(realSprite); + UnityEngine.Object.DestroyImmediate(errorSprite); + } + [Test] public void AddDebugConfigs_StoresAllProvided() { diff --git a/Tests/EditMode/Unit/DataServiceTest.cs b/Tests/EditMode/Unit/DataServiceTest.cs index accd0c9..a994f22 100644 --- a/Tests/EditMode/Unit/DataServiceTest.cs +++ b/Tests/EditMode/Unit/DataServiceTest.cs @@ -36,13 +36,26 @@ protected override void OnDataSaved(string key, object data, System.Type type) } } + private const string PersistentDataKey = nameof(PersistentData); + private DataService _dataService; [SetUp] public void Init() { _dataService = new DataService(); - PlayerPrefs.DeleteAll(); + // Only delete the key(s) this fixture's tests write (DataService.SaveData/SaveAllData key on + // typeof(T).Name) rather than PlayerPrefs.DeleteAll(), which would wipe unrelated keys shared + // across the whole EditMode PlayerPrefs store (e.g. PerformanceTestSetup's PT_Run/PT_Settings). + // RCR: DataService.cs LoadData — a stale PersistentDataKey value left by a prior run would + // make LoadData_NoExistingData_CreatesNew RED (loadedData.Name is the stale Name, not null). + PlayerPrefs.DeleteKey(PersistentDataKey); + } + + [TearDown] + public void Cleanup() + { + PlayerPrefs.DeleteKey(PersistentDataKey); } [Test] diff --git a/Tests/EditMode/Unit/ObjectPoolTest.cs b/Tests/EditMode/Unit/ObjectPoolTest.cs index 068d604..7fb71c3 100644 --- a/Tests/EditMode/Unit/ObjectPoolTest.cs +++ b/Tests/EditMode/Unit/ObjectPoolTest.cs @@ -21,7 +21,14 @@ public class MockEntity : IMockEntity { private IObjectPool _pool; - public void Init(IObjectPool pool) => _pool = pool; + public int InitCount { get; private set; } + public IObjectPool LastInitPool => _pool; + + public void Init(IObjectPool pool) + { + _pool = pool; + InitCount++; + } public bool Despawn() => _pool.Despawn(this); public void OnDespawn() {} @@ -97,6 +104,23 @@ public void EntityDespawn_Successfully() Assert.AreEqual(0, pool.SpawnedReadOnly.Count); } + [Test] + // ADMIT: ObjectPoolBase.CallInstantiator is the only site that calls IPoolEntityObject.Init(pool), + // so every instance the pool produces must be told which pool owns it. + // RCR: ObjectPool.cs CallInstantiator — delete `poolEntity?.Init(this);` → RED (InitCount stays 0 instead + // of 2, LastInitPool stays null). 2026-08-01 + public void Spawn_OnPoolEntityObject_CallsInitWithOwningPoolEveryTime() + { + MockEntity sharedEntity = null; + var pool = new ObjectPool(0, () => sharedEntity ??= new MockEntity()); + + pool.Spawn(); + pool.Spawn(); + + Assert.AreEqual(2, sharedEntity.InitCount); + Assert.AreSame(pool, sharedEntity.LastInitPool); + } + [Test] public void Despawn_NotSpawnedObject_ReturnsFalse() { diff --git a/Tests/EditMode/Unit/RngServiceTest.cs b/Tests/EditMode/Unit/RngServiceTest.cs index 770a3be..c136d9e 100644 --- a/Tests/EditMode/Unit/RngServiceTest.cs +++ b/Tests/EditMode/Unit/RngServiceTest.cs @@ -60,6 +60,34 @@ public void Range_MinGreaterThanMax_ThrowsException() Assert.Throws(() => _rngService.Range(10, 5)); } + [Test] + // ADMIT: RngService.Range(floatP,floatP,bool) must accept a degenerate closed range where min == max and + // maxInclusive is true, rather than rejecting it as inverted. + // RCR: RngService.cs Range — change `if (min > max || ...)` to `if (min >= max || ...)` → RED + // (Range(2.5f, 2.5f, true) throws IndexOutOfRangeException instead of returning 2.5f). 2026-07-31 + public void Range_FloatMinEqualsMaxWithMaxInclusive_ReturnsMin() + { + var minMax = (floatP)2.5f; + + floatP result = default; + Assert.DoesNotThrow(() => result = _rngService.Range(minMax, minMax, true)); + Assert.AreEqual(minMax, result); + } + + [Test] + // ADMIT: RngService.Range(floatP,floatP,bool) must throw for an exclusive (maxInclusive:false) empty range + // rather than falling through to the equal-bounds early return and returning min. + // RCR: RngService.cs Range — drop the whole `|| (!maxInclusive && ...)` disjunct, leaving `if (min > max)` + // → RED (returns 2.5f instead of throwing). Dropping only the `!maxInclusive &&` prefix is NOT valid here: + // the remaining epsilon term is true for min == max regardless, and it falsifies the inclusive sibling + // test instead. 2026-08-02 + public void Range_FloatMinEqualsMaxWithMaxExclusive_ThrowsIndexOutOfRange() + { + var minMax = (floatP)2.5f; + + Assert.Throws(() => _rngService.Range(minMax, minMax, false)); + } + [Test] public void Restore_ToPastCount_ReproducesSequence() { diff --git a/Tests/EditMode/Unit/VersionServicesTest.cs b/Tests/EditMode/Unit/VersionServicesTest.cs index bbb085b..36f2c53 100644 --- a/Tests/EditMode/Unit/VersionServicesTest.cs +++ b/Tests/EditMode/Unit/VersionServicesTest.cs @@ -1,6 +1,5 @@ using GameLovers.Services; using NUnit.Framework; -using UnityEngine; // ReSharper disable once CheckNamespace @@ -9,70 +8,6 @@ namespace GameLoversEditor.Services.Tests [TestFixture] public class VersionServicesTest { - /// - /// Testable version comparison logic extracted from VersionServices.IsOutdatedVersion. - /// Since IsOutdatedVersion uses Application.version (read-only in EditMode), - /// we extract the comparison logic here to enable unit testing. - /// - private static bool IsOutdatedVersionTestable(string appVersion, string otherVersion) - { - var appVersionParts = appVersion.Split('.'); - var otherVersionParts = otherVersion.Split('.'); - - var majorApp = int.Parse(appVersionParts[0]); - var majorOther = int.Parse(otherVersionParts[0]); - - var minorApp = int.Parse(appVersionParts[1]); - var minorOther = int.Parse(otherVersionParts[1]); - - var patchApp = int.Parse(appVersionParts[2]); - var patchOther = int.Parse(otherVersionParts[2]); - - if (majorApp != majorOther) - { - return majorOther > majorApp; - } - - if (minorApp != minorOther) - { - return minorOther > minorApp; - } - - return patchOther > patchApp; - } - - [Test] - public void IsOutdatedVersion_NewerMajor_ReturnsTrue() - { - Assert.That(IsOutdatedVersionTestable("1.0.0", "2.0.0"), Is.True); - } - - [Test] - public void IsOutdatedVersion_NewerMinor_ReturnsTrue() - { - Assert.That(IsOutdatedVersionTestable("1.1.0", "1.2.0"), Is.True); - } - - [Test] - public void IsOutdatedVersion_NewerPatch_ReturnsTrue() - { - Assert.That(IsOutdatedVersionTestable("1.1.1", "1.1.2"), Is.True); - } - - [Test] - public void IsOutdatedVersion_SameVersion_ReturnsFalse() - { - Assert.That(IsOutdatedVersionTestable("1.1.1", "1.1.1"), Is.False); - } - - [Test] - public void IsOutdatedVersion_OlderVersion_ReturnsFalse() - { - Assert.That(IsOutdatedVersionTestable("2.0.0", "1.0.0"), Is.False); - Assert.That(IsOutdatedVersionTestable("1.2.0", "1.1.0"), Is.False); - Assert.That(IsOutdatedVersionTestable("1.1.2", "1.1.1"), Is.False); - } - [Test] public void FormatInternalVersion_WithBuildType_IncludesBuildType() { @@ -84,7 +19,7 @@ public void FormatInternalVersion_WithBuildType_IncludesBuildType() BuildNumber = "1" }; var result = VersionServices.FormatInternalVersion(data); - + Assert.That(result.Contains("debug"), Is.True); Assert.That(result.Contains("abc"), Is.True); Assert.That(result.Contains("main"), Is.True); @@ -102,22 +37,9 @@ public void FormatInternalVersion_WithoutBuildType_OmitsBuildType() BuildNumber = "1" }; var result = VersionServices.FormatInternalVersion(data); - + Assert.That(result.EndsWith("."), Is.False); Assert.That(result.Contains("abc"), Is.True); } - - [Test] - public void IsOutdatedVersion_DirectInvocation_AgreesWithLocalVersion() - { - var current = Application.version; - - if (string.IsNullOrEmpty(current) || current.Split('.').Length < 3) - { - Assert.Inconclusive($"Application.version='{current}' is not a 3-part Major.Minor.Patch string; the production parser requires three parts."); - } - - Assert.IsFalse(VersionServices.IsOutdatedVersion(current)); - } } } diff --git a/Tests/PlayMode/Integration/VersionServicesIntegrationTest.cs b/Tests/PlayMode/Integration/VersionServicesIntegrationTest.cs index 56dd94e..bdfbb24 100644 --- a/Tests/PlayMode/Integration/VersionServicesIntegrationTest.cs +++ b/Tests/PlayMode/Integration/VersionServicesIntegrationTest.cs @@ -10,7 +10,10 @@ namespace GameLoversEditor.Services.Tests { /// /// Integration tests for that exercise the async resource-loading - /// pipeline and all post-load property accessors. + /// pipeline. The post-load property-accessor assertions and the auto-load-on-access assertion are + /// covered by the EditMode sync fixture () via the + /// synchronous path — this fixture is kept narrowly + /// scoped to the one thing that is genuinely distinct production code: the async load path. /// Requires Assets/Configs/Resources/version-data.txt to exist in the project. /// public class VersionServicesIntegrationTest @@ -24,22 +27,11 @@ public void ResetStaticState() LoadedField.SetValue(null, false); } - [UnityTest, Order(1)] - public IEnumerator AccessBeforeLoad_AutoLoads() - { - Assert.IsFalse((bool)LoadedField.GetValue(null), "Precondition: SetUp resets _loaded to false"); - - Assert.DoesNotThrow(() => { var _ = VersionServices.VersionInternal; }); - Assert.DoesNotThrow(() => { var _ = VersionServices.Branch; }); - Assert.DoesNotThrow(() => { var _ = VersionServices.Commit; }); - Assert.DoesNotThrow(() => { var _ = VersionServices.BuildNumber; }); - - Assert.IsTrue((bool)LoadedField.GetValue(null), "Accessor should auto-trigger LoadVersionData via EnsureLoaded"); - - yield return null; - } - - [UnityTest, Order(2)] + [UnityTest] + // ADMIT: VersionServices.LoadVersionDataAsync is the only async load path and no other test exercises it; + // broken TaskCompletionSource wiring would leave it never completing or never flipping the loaded flag. + // RCR: VersionServices.cs LoadVersionDataAsync — comment out + // `ApplyTextAsset(textAsset, asyncContext: true);` → RED (_loaded stays false). 2026-07-31 public IEnumerator LoadVersionDataAsync_Successfully() { var task = VersionServices.LoadVersionDataAsync(); @@ -51,65 +43,5 @@ public IEnumerator LoadVersionDataAsync_Successfully() Assert.IsTrue((bool)LoadedField.GetValue(null), "Version data should be loaded"); } - - [UnityTest, Order(3)] - public IEnumerator AfterLoad_VersionInternal_ContainsExpectedParts() - { - var task = VersionServices.LoadVersionDataAsync(); - while (!task.IsCompleted) yield return null; - - var version = VersionServices.VersionInternal; - - Assert.IsNotNull(version); - Assert.IsNotEmpty(version); - Assert.IsTrue(version.Contains("."), "VersionInternal should contain version separators"); - } - - [UnityTest, Order(4)] - public IEnumerator AfterLoad_Branch_ReturnsNonEmptyString() - { - var task = VersionServices.LoadVersionDataAsync(); - while (!task.IsCompleted) yield return null; - - var branch = VersionServices.Branch; - - Assert.IsNotNull(branch); - Assert.IsNotEmpty(branch); - } - - [UnityTest, Order(5)] - public IEnumerator AfterLoad_Commit_ReturnsNonEmptyString() - { - var task = VersionServices.LoadVersionDataAsync(); - while (!task.IsCompleted) yield return null; - - var commit = VersionServices.Commit; - - Assert.IsNotNull(commit); - Assert.IsNotEmpty(commit); - } - - [UnityTest, Order(6)] - public IEnumerator AfterLoad_BuildNumber_ReturnsNonEmptyString() - { - var task = VersionServices.LoadVersionDataAsync(); - while (!task.IsCompleted) yield return null; - - var buildNumber = VersionServices.BuildNumber; - - Assert.IsNotNull(buildNumber); - Assert.IsNotEmpty(buildNumber); - } - - [UnityTest, Order(7)] - public IEnumerator VersionExternal_AlwaysAccessible_WithoutLoad() - { - var external = VersionServices.VersionExternal; - - Assert.IsNotNull(external); - Assert.IsNotEmpty(external); - - yield return null; - } } } diff --git a/Tests/PlayMode/Unit/CoroutineServiceTest.cs b/Tests/PlayMode/Unit/CoroutineServiceTest.cs index 0dfb896..d59ef31 100644 --- a/Tests/PlayMode/Unit/CoroutineServiceTest.cs +++ b/Tests/PlayMode/Unit/CoroutineServiceTest.cs @@ -142,6 +142,36 @@ public IEnumerator StopAllCoroutines_Successfully() Assert.AreNotEqual(testValue3, _testValue); } + [UnityTest] + // ADMIT: CoroutineService.StopCoroutine guards a null `coroutine` handle before forwarding to + // _serviceObject.ExternalStopCoroutine. + // RCR: CoroutineService.cs StopCoroutine — remove the `coroutine == null ||` term from the guard → RED + // (falls through to MonoBehaviour.StopCoroutine(null), which throws). 2026-08-01 + public IEnumerator StopCoroutine_NullCoroutine_DoesNotThrow() + { + Assert.DoesNotThrow(() => _coroutineService.StopCoroutine(null)); + + yield return null; + } + + [UnityTest] + // ADMIT: CoroutineService.StopCoroutine also guards the host _serviceObject being Unity fake-null (native + // object destroyed while the C# reference survives) — distinct from Dispose(), which assigns a real null. + // This test destroys the host GameObject directly to reproduce that fake-null path. + // RCR: CoroutineService.cs StopCoroutine — remove the `_serviceObject == null ||` term from the guard → + // RED (MissingReferenceException from `_serviceObject.gameObject`). 2026-08-01 + public IEnumerator StopCoroutine_AfterServiceObjectDestroyed_DoesNotThrowMissingReference() + { + var coroutine = _coroutineService.StartCoroutine(TestCoroutine(5)); + var host = Object.FindObjectsByType()[0]; + + Object.Destroy(host.gameObject); + + yield return null; // Allow the native destruction to be reflected on the host reference + + Assert.DoesNotThrow(() => _coroutineService.StopCoroutine(coroutine)); + } + [UnityTest] public IEnumerator Dispose_DestroysHostGameObject() { diff --git a/Tests/PlayMode/Unit/GameObjectPoolTest.cs b/Tests/PlayMode/Unit/GameObjectPoolTest.cs index ec5f177..35de3a8 100644 --- a/Tests/PlayMode/Unit/GameObjectPoolTest.cs +++ b/Tests/PlayMode/Unit/GameObjectPoolTest.cs @@ -142,5 +142,39 @@ public IEnumerator Dispose_AfterDespawnedInstanceDestroyedExternally_DoesNotThro Assert.DoesNotThrow(() => _pool.Dispose()); } + + [UnityTest] + // ADMIT: GameObjectPool.Dispose(bool) destroyed SampleEntity unconditionally, ignoring disposeSampleEntity, + // so Dispose(false) still destroyed a sample entity the caller explicitly asked to keep. + // RCR: GameObjectPool.cs Dispose — revert to unconditional `Object.Destroy(SampleEntity);` → RED (the + // sample entity is destroyed even with disposeSampleEntity: false). 2026-08-01 + public IEnumerator Dispose_WithDisposeSampleEntityFalse_DoesNotDestroySampleEntity() + { + _pool.Dispose(false); + + yield return null; + + Assert.IsFalse(_sample == null); + } + + [UnityTest] + // ADMIT: ObjectPoolBase.SpawnEntity retries popping while the popped entity is Unity fake-null, so a + // pooled GameObject destroyed by an external owner while despawned is never handed back out. + // RCR: ObjectPool.cs SpawnEntity — collapse the do-while retry to a single unconditional pop → RED (Spawn + // returns the destroyed instance; IsFalse(freshInstance == null) fails). 2026-08-01 + public IEnumerator Spawn_WhenPooledEntityWasDestroyedExternally_ReturnsFreshInstance() + { + var instance = _pool.Spawn(); + _pool.Despawn(instance); + + Object.DestroyImmediate(instance); + + var freshInstance = _pool.Spawn(); + + Assert.IsFalse(freshInstance == null); + Assert.AreNotSame(instance, freshInstance); + + yield return null; + } } } diff --git a/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs b/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs index 760f330..108c154 100644 --- a/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs +++ b/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs @@ -42,6 +42,20 @@ public void Cleanup() if (_sampleGo != null) Object.Destroy(_sampleGo); } + [UnityTest] + // ADMIT: GameObjectPool.Dispose(bool) destroyed SampleEntity.gameObject unconditionally — the same bug + // already fixed on the non-generic GameObjectPool (see the sibling test in GameObjectPoolTest.cs). + // RCR: GameObjectPool.cs GameObjectPool.Dispose — revert to unconditional + // `Object.Destroy(SampleEntity.gameObject);` → RED (_sampleGo destroyed with disposeSampleEntity: false). 2026-08-01 + public IEnumerator Dispose_WithDisposeSampleEntityFalse_DoesNotDestroySampleEntity() + { + _pool.Dispose(false); + + yield return null; + + Assert.IsFalse(_sampleGo == null); + } + [UnityTest] public IEnumerator Spawn_ReturnsComponentReference() { diff --git a/Tests/PlayMode/Unit/TickServiceTest.cs b/Tests/PlayMode/Unit/TickServiceTest.cs index b5a250f..fa22fe3 100644 --- a/Tests/PlayMode/Unit/TickServiceTest.cs +++ b/Tests/PlayMode/Unit/TickServiceTest.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Generic; using GameLovers.Services; using NUnit.Framework; using UnityEngine; @@ -63,6 +64,30 @@ public IEnumerator SubscribeOnUpdate_TimeOverflow_CarriesOverflow() Assert.GreaterOrEqual(callCount, 2); } + [UnityTest] + // ADMIT: TickService.Update special-cases `tickData.DeltaTime == 0` so the overflow calc never evaluates + // `deltaTime % 0`. Float modulo-by-zero yields NaN (not an exception), and once NaN reaches LastTickTime + // every later comparison is unordered-false, feeding the subscriber NaN deltaTime on every tick. + // RCR: TickService.cs Update — drop the zero-guard, leaving `var overFlow = deltaTime % tickData.DeltaTime;` + // → RED (the NaN assertion below fails from the 2nd received deltaTime on). A callCount-only assertion + // would NOT redden: NaN comparisons are always false, so ticking never stops — it just carries NaN. 2026-08-01 + public IEnumerator SubscribeOnUpdate_ZeroDeltaTimeWithOverflowToNextTick_TicksEveryFrame() + { + var deltaTimes = new List(); + _tickService.SubscribeOnUpdate(dt => deltaTimes.Add(dt), deltaTime: 0f, timeOverflowToNextTick: true); + + yield return null; + yield return null; + yield return null; + + Assert.AreEqual(3, deltaTimes.Count); + + foreach (var dt in deltaTimes) + { + Assert.IsFalse(float.IsNaN(dt), "Received deltaTime should never be NaN"); + } + } + [UnityTest] public IEnumerator SubscribeOnUpdate_RealTime_UsesUnscaledTime() { diff --git a/package.json b/package.json index c640f29..eff1a49 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "dependencies": { "com.gamelovers.gamedata": "1.0.0", "com.unity.addressables": "1.21.20", - "com.cysharp.unitask": "2.5.10" + "com.cysharp.unitask": "2.5.10", + "com.unity.test-framework.performance": "3.5.0" }, "samples": [ { From a0edbb3f6c6cc988269f8dcf2fd7c1b775aa0fd4 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Sun, 2 Aug 2026 19:57:27 +0100 Subject: [PATCH 07/32] =?UTF-8?q?docs:=20add=20UNFALSIFIABLE=20exemption?= =?UTF-8?q?=20and=20per-class=20verdicts=20to=20RCR=20(shared=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §2 declared any test without an RCR line "suspect by default", but some correct tests provably have no one-line mutation - double-guarded validation, where an unconfigured object trips two independent guards so disabling either leaves the other throwing. The rule was mislabelling tests that are right and unbreakable. Adds an UNFALSIFIABLE exemption on §13's terms: the reason must be falsifiable, must name both guards, and must record that a mutation was tried and observed green. "Couldn't find one" is explicitly not a reason - that is an unfinished RCR, not an exemption. Also adds a verdict table for tests that resist mutation, because they are not one problem: A5 duplicates get deleted (naming the surviving sibling), D2 overclaims get a strengthened assertion or an honest rename, and UNFALSIFIABLE tests are kept with the exemption comment. The class must be proven before acting - an A5 duplicate by observing the sibling's mutation redden both, a D2 overclaim by observing the implied mutation leave the test green. §1 and §2 remain byte-identical across all six packages. Co-Authored-By: Claude Opus 5 --- Tests/AGENTS.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index 120c37e..9c78df0 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -95,10 +95,38 @@ dead end, and it cannot be recovered from the code. Also add one line per new test to the commit body: `RCR: `. That makes `git log --grep=RCR` the audit surface. +**UNFALSIFIABLE — the one honest exemption.** Some correct tests provably have no +one-line mutation. The commonest case is **double-guarded validation**: an +unconfigured object trips two independent guards, so disabling either leaves the +other throwing. Deleting such a test would lose real coverage, so it is exempt — +but only on the same terms as §13, never as a shrug: + +```csharp +// RCR: none exists — trips both and ; disabling either +// leaves the other throwing (verified). Double-covered, not single-line falsifiable. +``` + +The reason must be falsifiable and must record that a mutation was actually tried +and observed green. "Couldn't find one" is not a reason — that is an unfinished RCR, +not an exemption. + +**Verdicts for a test that resists mutation.** Work out which of three it is; they +have different answers: + +| Finding | Test | Action | +|---|---|---| +| **A5 duplicate** — the only mutation that reddens it already belongs to a sibling | pins nothing new | **Delete**, naming the surviving sibling in the commit body | +| **D2 overclaim** — the name promises behaviour the body cannot detect | name is a lie | **Strengthen the assertion**, or rename to what it actually checks | +| **UNFALSIFIABLE** — real behaviour, but double-guarded or otherwise unbreakable one line at a time | valid | **Keep**, with the exemption comment above | + +Prove the class before acting. An A5 duplicate is confirmed when the sibling's +mutation is observed reddening both; a D2 overclaim is confirmed when the mutation +the name implies leaves the test green. + **Two consequences, stated so RCR does not become theatre:** -- A test with no `// RCR:` line is not trusted coverage. In an audit it is a - suspect by default. +- A test with no `// RCR:` line — and no UNFALSIFIABLE exemption — is not trusted + coverage. In an audit it is a suspect by default. - **Benchmarks are included, inverted:** a performance test must be observed *changing its number* when the measured operation is removed from the measured body. A benchmark whose measured region does not contain the workload is a From 4afc7d6e910a04e978162597c4f915829fd7d8c3 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Sun, 2 Aug 2026 21:14:08 +0100 Subject: [PATCH 08/32] docs: add the A3-reject verdict class to the RCR exemption rules Section 2's verdicts table gains a fourth class. UNFALSIFIABLE was absorbing tests that section 1's A3 rule would never have admitted - the tell being reasons like "no line in Runtime/ participates" or "these are C#'s zero-init values", which describe a test that pins nothing rather than one that is hard to break. New rule: if no line in Runtime/ or Editor/ participates in the assertion, it is an A3 reject and the verdict is delete. UNFALSIFIABLE stays reserved for behaviour this package genuinely owns but cannot be broken one line at a time. Sections 1-2 are shared verbatim across all six Tests/AGENTS.md; no test or production changes in this package. Co-Authored-By: Claude Opus 5 --- Tests/AGENTS.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index 9c78df0..78703f5 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -110,18 +110,29 @@ The reason must be falsifiable and must record that a mutation was actually trie and observed green. "Couldn't find one" is not a reason — that is an unfinished RCR, not an exemption. -**Verdicts for a test that resists mutation.** Work out which of three it is; they +**Verdicts for a test that resists mutation.** Work out which of four it is; they have different answers: | Finding | Test | Action | |---|---|---| +| **A3 reject** — no line in `Runtime/` or `Editor/` participates; the assertion is C#- or Unity-guaranteed | pins nothing, ever | **Delete** | | **A5 duplicate** — the only mutation that reddens it already belongs to a sibling | pins nothing new | **Delete**, naming the surviving sibling in the commit body | | **D2 overclaim** — the name promises behaviour the body cannot detect | name is a lie | **Strengthen the assertion**, or rename to what it actually checks | | **UNFALSIFIABLE** — real behaviour, but double-guarded or otherwise unbreakable one line at a time | valid | **Keep**, with the exemption comment above | +**A3 is checked first, and it is the commonest way the exemption gets abused.** +UNFALSIFIABLE is for behaviour this package genuinely owns but cannot be broken one +line at a time. It is *never* for behaviour the package does not own. The tell is in +the reason itself: if you find yourself writing "no line in Runtime/ participates", +"the only edit is a compile error", or "these are C#'s zero-init values", you have +found an A3 reject and the verdict is **delete** — a field-only struct's assignment +and default values are the language's guarantees, not yours. Writing that sentence +under an UNFALSIFIABLE heading launders a test §1 would never have admitted. + Prove the class before acting. An A5 duplicate is confirmed when the sibling's mutation is observed reddening both; a D2 overclaim is confirmed when the mutation -the name implies leaves the test green. +the name implies leaves the test green; an A3 reject is confirmed when no production +symbol appears anywhere in the causal chain behind the assertion. **Two consequences, stated so RCR does not become theatre:** From 1dc48be2bdbc0dcbc1412ed4ebee7c4227ab4b4f Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Sun, 2 Aug 2026 21:19:14 +0100 Subject: [PATCH 09/32] docs: record the first trustworthy coverage baseline (runtime 84.4%) Section 13 now carries a dated baseline for this package's runtime assembly, plus the reason to steer by that number rather than the combined one. Every earlier coverage figure in this repo was an artifact and must not be compared against: - reports before today ran without -debugCodeOptimization, so Unity compiled Release and emitted ~40% fewer sequence points (MathfloatP showed 637 coverable lines instead of 1002) - a silently shrunken denominator - some runs leaked test and sample assemblies into scope, and some covered only 3 of the 6 packages The current run covers all 11 production assemblies with none leaking, verified via the MathfloatP denominator check now documented in Tools/coverage.sh. Repo-wide: runtime 73.9%, Editor 5.5%, combined 41.0%. Editor is 48.1% of all coverable lines and is accepted-untestable per the ACCEPTED (iii) rows in section 13, which is the whole reason the combined figure is not the one to track. Co-Authored-By: Claude Opus 5 --- Tests/AGENTS.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index 78703f5..9bff60c 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -234,6 +234,16 @@ Why both keys: `RunSettings.Instance` is a lazy-loaded singleton (`ResourcesLoad ## 13. Coverage Register +**Baseline — runtime assembly: 84.4% (1050/1244), measured 2026-08-02.** +Editor assembly: **4.8% (153/3162)** — near-zero by policy; the ACCEPTED (iii) rows below are why. + +Regenerate with `Tools/coverage.sh`, which prints the runtime/Editor split. +Steer by the **runtime** figure: Editor code is ~48% of the repo's coverable +lines and is accepted-untestable, so the combined number (41.0%) can never +meaningfully move. Do not compare against any figure recorded before this date — +earlier reports were produced without `-debugCodeOptimization` (Release mode +shrinks the denominator ~40%) or with test/sample assemblies leaking into scope. + Every untested symbol worth naming is either ACCEPTED (justified — do not re-report) or OPEN (a real gap, owed a test). An untested symbol in neither state is an audit finding. From b0d1b61a46e138dc6c25ced82a15528b01ccb158 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Sun, 2 Aug 2026 22:30:36 +0100 Subject: [PATCH 10/32] =?UTF-8?q?style:=20use=20classic=20asserts=20in=20V?= =?UTF-8?q?ersionServicesTest=20per=20Tests/AGENTS.md=20=C2=A79?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §9 permits Assert.That ONLY for tolerance/range constraints classic asserts cannot express, authorising exactly TimeServiceTest.cs:67,83 and calling any other use a review reject. These six were Assert.That(x.Contains(...), Is.True) / Is.False - plain boolean checks that Assert.IsTrue/IsFalse express directly, so they were outside the exemption on its own terms. No behavioural change; EditMode green. Co-Authored-By: Claude Opus 5 --- Tests/EditMode/Unit/VersionServicesTest.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/EditMode/Unit/VersionServicesTest.cs b/Tests/EditMode/Unit/VersionServicesTest.cs index 36f2c53..20ea6f9 100644 --- a/Tests/EditMode/Unit/VersionServicesTest.cs +++ b/Tests/EditMode/Unit/VersionServicesTest.cs @@ -20,10 +20,10 @@ public void FormatInternalVersion_WithBuildType_IncludesBuildType() }; var result = VersionServices.FormatInternalVersion(data); - Assert.That(result.Contains("debug"), Is.True); - Assert.That(result.Contains("abc"), Is.True); - Assert.That(result.Contains("main"), Is.True); - Assert.That(result.Contains("1"), Is.True); + Assert.IsTrue(result.Contains("debug")); + Assert.IsTrue(result.Contains("abc")); + Assert.IsTrue(result.Contains("main")); + Assert.IsTrue(result.Contains("1")); } [Test] @@ -38,8 +38,8 @@ public void FormatInternalVersion_WithoutBuildType_OmitsBuildType() }; var result = VersionServices.FormatInternalVersion(data); - Assert.That(result.EndsWith("."), Is.False); - Assert.That(result.Contains("abc"), Is.True); + Assert.IsFalse(result.EndsWith(".")); + Assert.IsTrue(result.Contains("abc")); } } } From 89078f468b7ab0154b63c5381938f0b0798b3dc9 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Sun, 2 Aug 2026 22:31:34 +0100 Subject: [PATCH 11/32] docs: add the A6 ENVIRONMENT admission criterion Sections 1-2 are shared verbatim across all six packages; this adds a sixth admission question and the worked instance behind it. A6 asks whether an assertion's outcome would change if project configuration changed - a renderer feature installed or removed, an Addressables catalog built, a sample imported. If so the test must READ that state rather than assume one value of it. A6 is not A3. A3 asks whether the package computed the value; A6 asks whether the test assumed which value it would be. A test can satisfy A3 and still fail A6, which is exactly how the gap went unnoticed: UiBackdropBlurPresenterFeatureTests read a package-computed flag (UiBackdropBlurRendererFeature.IsInstalled) but hard-coded the expectation that it was false. Batchmode never instantiates the URP renderer, so the flag was false there and all five tests passed; in the Editor the feature registers from the project's renderer asset and all five failed. The fixture was asserting a fact about the repo, not about the code under test. Validated against the existing corpus before being written, per root AGENTS.md 2.2: the blur fixture was the only violation and is already fixed. AddressablesUiAssetLoaderTests asserts on a key that is unresolvable either way, and UiCameraStackFeatureTests builds its own cameras rather than reading project renderer state. Co-Authored-By: Claude Opus 5 --- Tests/AGENTS.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index 9bff60c..00e5d42 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -18,6 +18,31 @@ as comments on the test itself. | **A3 PACKAGE** | Does every assertion read a value this package computed? Reject assertions on `new X() != new X()`, `!= null` on a freshly constructed object, default struct/enum values, or anything the C# spec or the Unity engine already guarantees. | | **A4 CHEAPEST** | Is this the cheapest tier that covers the defect? EditMode beats PlayMode; a `[TestCase]` row on an existing fixture beats a new `[Test]`; a new `[Test]` beats a new fixture. Grep before writing. | | **A5 UNIQUE** | Does no existing test already fail on the A2 edit? Grep the symbol under test across `Tests/` first. | +| **A6 ENVIRONMENT** | Would this assertion's outcome change if project configuration changed — a renderer feature installed or removed, an Addressables catalog built, a sample imported, a quality tier switched? If yes, the test must **read** that state, not assume one value of it. | + +**A6 in practice.** A6 is not A3. A3 asks whether the package computed the value; A6 +asks whether the test assumed which value it would be. A test can satisfy A3 and still +fail A6 — reading a package-computed flag is fine, hard-coding the expectation that the +flag is `false` is not. + +The concrete instance: `UiBackdropBlurPresenterFeatureTests` unconditionally expected +the "no renderer feature installed" error. Production only logs it when +`UiBackdropBlurRendererFeature.IsInstalled` is false. Batchmode never instantiates the +URP renderer, so the flag was false and all five tests passed; in the Editor the feature +registers from the project's renderer asset, the flag is true, production correctly stays +silent, and all five failed. The fixture was really asserting *"this project has no blur +renderer feature"* — a fact about the repo, not about the code under test. + +The fix shape is always the same: branch the expectation on the state instead of assuming +it, and leave the assertions that are actually the subject untouched. + +```csharp +if (UiBackdropBlurRendererFeature.IsInstalled) return; // production logs nothing +LogAssert.Expect(LogType.Error, ...); +``` + +If a test genuinely needs one specific value of ambient state, it must establish that +state itself in `[SetUp]` and restore it in `[TearDown]` — never inherit it. **A5-bis — inherited-type coverage.** Before proposing a fixture for a type that derives from or wraps another tested type, grep `Tests/` for the derived type's From dcb8c2548eff4825f2a3b5261dca1310131dfa91 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Mon, 3 Aug 2026 00:25:47 +0100 Subject: [PATCH 12/32] test: RCR-backfill 122 services EditMode tests 118 of 122 mutations went RED on the first pass; the remaining four were investigated rather than dropped, and three of them turned out to be defects in the TESTS, exposed by the mutation staying green. - UnityTime_Convertions_Successfully and UnixTime_Convertions_Successfully assert Assert.GreaterOrEqual(ErrorValue, converted - now), which bounds the difference only from ABOVE. Shrinking mutations are invisible: negating _initialUnityTime, and swapping TotalMilliseconds for TotalSeconds (a 1000x shrink), were both observed GREEN. Growing mutations redden immediately. Both want a two-sided bound on the absolute difference; recorded on the tests as a negative result until that is decided. - Peekfloat_DoesNotAdvanceState is precision-blind. Peekfloat draws over (0, floatP.MaxValue), where consecutive draws saturate to the same floatP - so making PeekRange consume the LIVE state stayed green, and the test cannot see the advance its name claims to catch. Narrowing the range reddens it. - TryResolve_NotBound_ReturnsFalse's first mutation (`return true;`) left the `out` parameter unassigned, so it failed to COMPILE and the harness reported no results rather than a red. `instance = default; return true;` is the valid form. 41 tests were left without a mutation, classified with evidence rather than given an invented one: A3 rejects where the assertion compares two runs of the same code (RngService determinism pairs) or reads Unity/BCL guarantees; A5 duplicates whose only falsifying edit belongs to a named sibling; and D2 overclaims that assert only DoesNotThrow - notably five AssetResolverServiceTest cases where the registration-failure signal is a Debug.LogWarning, which Unity's runner does not fail on. Two OPEN gaps found in passing: ObjectPool.Despawn's `onlyFirst` break is uncovered repo-wide (the EditMode test spawns one entity, the PlayMode sibling's predicate matches one), and DataServiceTest.ReplaceData_Successfully never enters the replace branch at all because its two payloads have different T. EditMode suite green at 826/826. Co-Authored-By: Claude Opus 5 --- Tests/EditMode/Unit/AddressableConfigTest.cs | 23 ++++++++ .../Unit/AddressableIdsEditorSettingsTest.cs | 51 +++++++++++++++++ .../Unit/AssetConfigsScriptableObjectTest.cs | 4 ++ Tests/EditMode/Unit/AssetLoaderUtilsTest.cs | 4 ++ .../EditMode/Unit/AssetResolverServiceTest.cs | 27 +++++++++ .../Unit/AssetsImporterEditorSettingsTest.cs | 4 ++ Tests/EditMode/Unit/CommandServiceTest.cs | 4 ++ Tests/EditMode/Unit/DataServiceTest.cs | 26 +++++++++ Tests/EditMode/Unit/InstallerTest.cs | 21 +++++++ Tests/EditMode/Unit/MainInstallerTest.cs | 15 +++++ .../EditMode/Unit/MessageBrokerServiceTest.cs | 39 +++++++++++++ Tests/EditMode/Unit/ObjectPoolTest.cs | 51 +++++++++++++++++ Tests/EditMode/Unit/PoolServiceTest.cs | 35 ++++++++++++ Tests/EditMode/Unit/RngServiceTest.cs | 57 +++++++++++++++++++ Tests/EditMode/Unit/TimeServiceTest.cs | 24 ++++++++ .../Unit/VersionServicesSyncLoadTest.cs | 20 +++++++ Tests/EditMode/Unit/VersionServicesTest.cs | 7 +++ .../Unit/VersioningEditorSettingsTest.cs | 17 ++++++ 18 files changed, 429 insertions(+) diff --git a/Tests/EditMode/Unit/AddressableConfigTest.cs b/Tests/EditMode/Unit/AddressableConfigTest.cs index 64ab798..37b7e33 100644 --- a/Tests/EditMode/Unit/AddressableConfigTest.cs +++ b/Tests/EditMode/Unit/AddressableConfigTest.cs @@ -22,18 +22,30 @@ public void Init() } [Test] + // ADMIT: AddressableConfig.GetSceneName starts the substring one character past the last '/' so the separator is + // not part of the name. + // RCR: AddressableConfig.cs GetSceneName — drop the `+ 1` past the slash → RED ("/MainMenu" instead of + // "MainMenu"). Also reddens GetSceneName_WithAddressWithoutExtension. 2026-08-02 public void GetSceneName_WithSceneAssetType_ReturnsName() { Assert.AreEqual("MainMenu", _sceneConfig.GetSceneName()); } [Test] + // ADMIT: AddressableConfig.GetSceneName rejects a config whose AssetType is not Scene instead of returning a bogus + // name. + // RCR: AddressableConfig.cs GetSceneName — guard on `AssetType == null` instead → RED (the Sprite config returns + // "hero" and no InvalidOperationException is thrown). 2026-08-02 public void GetSceneName_WithNonSceneAssetType_Throws() { Assert.Throws(() => _spriteConfig.GetSceneName()); } [Test] + // ADMIT: AddressableConfigComparer.Equals compares configs by Id, so two configs sharing an Id are equal + // regardless of address. + // RCR: AddressableConfig.cs AddressableConfigComparer.Equals — invert the Id comparison → RED (Assert.IsTrue fails + // for two Id-0 configs). 2026-08-02 public void AddressableConfigComparer_EqualIds_ReturnsTrue() { var comparer = new AddressableConfigComparer(); @@ -43,6 +55,10 @@ public void AddressableConfigComparer_EqualIds_ReturnsTrue() } [Test] + // ADMIT: AddressableConfigComparer.GetHashCode returns the config Id so it stays consistent with the Id-based + // Equals. + // RCR: AddressableConfig.cs AddressableConfigComparer.GetHashCode — return a constant 0 → RED (the Id-1 config + // hashes to 0, not 1). 2026-08-02 public void AddressableConfigComparer_GetHashCode_ReturnsId() { var comparer = new AddressableConfigComparer(); @@ -52,6 +68,9 @@ public void AddressableConfigComparer_GetHashCode_ReturnsId() } [Test] + // ADMIT: AddressableConfig.GetSceneName falls back to index 0 when the address has no '/' separator. + // RCR: AddressableConfig.cs GetSceneName — start the no-slash fallback at 1 → RED ("ainMenu" instead of + // "MainMenu"); the slash-bearing siblings stay green. 2026-08-02 public void GetSceneName_WithAddressWithoutSlash_ReturnsFullAddress() { var rootSceneConfig = new AddressableConfig(2, "MainMenu.unity", "Assets/MainMenu.unity", @@ -61,6 +80,10 @@ public void GetSceneName_WithAddressWithoutSlash_ReturnsFullAddress() } [Test] + // ADMIT: AddressableConfig.GetSceneName clamps the end index to Address.Length when the address carries no '.' + // extension. + // RCR: AddressableConfig.cs GetSceneName — clamp to Address.Length - 1 → RED ("MyScen" instead of "MyScene"); the + // extension-bearing siblings stay green. 2026-08-02 public void GetSceneName_WithAddressWithoutExtension_ReturnsFullAddress() { var noExtensionSceneConfig = new AddressableConfig(3, "Scenes/MyScene", "Assets/Scenes/MyScene", diff --git a/Tests/EditMode/Unit/AddressableIdsEditorSettingsTest.cs b/Tests/EditMode/Unit/AddressableIdsEditorSettingsTest.cs index 1a83554..6cfcb31 100644 --- a/Tests/EditMode/Unit/AddressableIdsEditorSettingsTest.cs +++ b/Tests/EditMode/Unit/AddressableIdsEditorSettingsTest.cs @@ -33,6 +33,9 @@ public void RestoreOriginalSettings() // ---- IsValidIdentifier ---- [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidIdentifier rejects an empty identifier before touching trimmed[0]. + // RCR: AddressableIdsEditorSettings.cs IsValidIdentifier — return true from the empty branch → RED (Assert.IsFalse + // fails). Also reddens the whitespace-only sibling. 2026-08-02 public void IsValidIdentifier_EmptyString_ReturnsFalse() { Assert.IsFalse(AddressableIdsEditorSettings.IsValidIdentifier("", out var error)); @@ -40,6 +43,10 @@ public void IsValidIdentifier_EmptyString_ReturnsFalse() } [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidIdentifier uses IsNullOrWhiteSpace, so an all-whitespace identifier + // is rejected before trimmed[0] is indexed. + // RCR: AddressableIdsEditorSettings.cs IsValidIdentifier — weaken the guard to IsNullOrEmpty → RED + // (IndexOutOfRangeException on trimmed[0] for " "). 2026-08-02 public void IsValidIdentifier_WhitespaceOnly_ReturnsFalse() { Assert.IsFalse(AddressableIdsEditorSettings.IsValidIdentifier(" ", out var error)); @@ -47,6 +54,10 @@ public void IsValidIdentifier_WhitespaceOnly_ReturnsFalse() } [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidIdentifier rejects a leading digit, which C# forbids in an enum + // member name. + // RCR: AddressableIdsEditorSettings.cs IsValidIdentifier — return true from the leading-digit branch → RED + // ("1AddressableId" is accepted). Also reddens IsValidNamespace_SegmentStartsWithDigit. 2026-08-02 public void IsValidIdentifier_StartsWithDigit_ReturnsFalse() { Assert.IsFalse(AddressableIdsEditorSettings.IsValidIdentifier("1AddressableId", out var error)); @@ -54,6 +65,10 @@ public void IsValidIdentifier_StartsWithDigit_ReturnsFalse() } [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidIdentifier permits only letters, digits and underscore, so a dot is + // rejected. + // RCR: AddressableIdsEditorSettings.cs IsValidIdentifier — whitelist '.' in the character loop → RED + // ("Addressable.Id" is accepted). 2026-08-02 public void IsValidIdentifier_ContainsDot_ReturnsFalse() { Assert.IsFalse(AddressableIdsEditorSettings.IsValidIdentifier("Addressable.Id", out var error)); @@ -61,6 +76,10 @@ public void IsValidIdentifier_ContainsDot_ReturnsFalse() } [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidIdentifier permits only letters, digits and underscore, so a hyphen + // is rejected. + // RCR: AddressableIdsEditorSettings.cs IsValidIdentifier — whitelist '-' in the character loop → RED + // ("Addressable-Id" is accepted). 2026-08-02 public void IsValidIdentifier_ContainsHyphen_ReturnsFalse() { Assert.IsFalse(AddressableIdsEditorSettings.IsValidIdentifier("Addressable-Id", out var error)); @@ -68,6 +87,9 @@ public void IsValidIdentifier_ContainsHyphen_ReturnsFalse() } [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidIdentifier leaves `error` null on the success path. + // RCR: AddressableIdsEditorSettings.cs IsValidIdentifier — seed `error` to "" instead of null → RED + // (Assert.IsNull(error) fails). Also reddens the underscore-prefix sibling. 2026-08-02 public void IsValidIdentifier_ValidDefault_ReturnsTrue() { Assert.IsTrue(AddressableIdsEditorSettings.IsValidIdentifier("AddressableId", out var error)); @@ -75,6 +97,9 @@ public void IsValidIdentifier_ValidDefault_ReturnsTrue() } [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidIdentifier explicitly allows underscore alongside letters and digits. + // RCR: AddressableIdsEditorSettings.cs IsValidIdentifier — drop the `c != '_'` exemption → RED ("_AddressableId" + // is rejected). 2026-08-02 public void IsValidIdentifier_UnderscorePrefix_ReturnsTrue() { Assert.IsTrue(AddressableIdsEditorSettings.IsValidIdentifier("_AddressableId", out var error)); @@ -84,6 +109,9 @@ public void IsValidIdentifier_UnderscorePrefix_ReturnsTrue() // ---- IsValidNamespace ---- [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidNamespace rejects an empty namespace before splitting on '.'. + // RCR: AddressableIdsEditorSettings.cs IsValidNamespace — return true from the empty branch → RED (Assert.IsFalse + // fails). Also reddens the whitespace-only sibling. 2026-08-02 public void IsValidNamespace_EmptyString_ReturnsFalse() { Assert.IsFalse(AddressableIdsEditorSettings.IsValidNamespace("", out var error)); @@ -98,6 +126,10 @@ public void IsValidNamespace_WhitespaceOnly_ReturnsFalse() } [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidNamespace rejects an empty segment, which is what a trailing dot + // produces. + // RCR: AddressableIdsEditorSettings.cs IsValidNamespace — return true from the empty-segment branch → RED + // ("Game.Ids." is accepted). Also reddens the consecutive-dots sibling. 2026-08-02 public void IsValidNamespace_TrailingDot_ReturnsFalse() { Assert.IsFalse(AddressableIdsEditorSettings.IsValidNamespace("Game.Ids.", out var error)); @@ -112,6 +144,10 @@ public void IsValidNamespace_ConsecutiveDots_ReturnsFalse() } [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidNamespace validates each dot-separated segment through + // IsValidIdentifier. + // RCR: AddressableIdsEditorSettings.cs IsValidNamespace — return true from the invalid-segment branch → RED + // ("Game.1Ids" is accepted). 2026-08-02 public void IsValidNamespace_SegmentStartsWithDigit_ReturnsFalse() { Assert.IsFalse(AddressableIdsEditorSettings.IsValidNamespace("Game.1Ids", out var error)); @@ -119,6 +155,9 @@ public void IsValidNamespace_SegmentStartsWithDigit_ReturnsFalse() } [Test] + // ADMIT: AddressableIdsEditorSettings.IsValidNamespace accepts a well-formed dot-separated namespace. + // RCR: AddressableIdsEditorSettings.cs IsValidNamespace — return false from the success path → RED ("Game.Ids" is + // rejected). Also reddens the single-segment and deep-hierarchy siblings. 2026-08-02 public void IsValidNamespace_ValidDefault_ReturnsTrue() { Assert.IsTrue(AddressableIdsEditorSettings.IsValidNamespace("Game.Ids", out var error)); @@ -142,6 +181,10 @@ public void IsValidNamespace_DeepHierarchy_ReturnsTrue() // ---- Setter normalization ---- [Test] + // ADMIT: AddressableIdsEditorSettings.ScriptFilename's setter trims surrounding whitespace and substitutes the + // default for null. + // RCR: AddressableIdsEditorSettings.cs ScriptFilename setter — drop the `.Trim()` → RED (the padded value is + // stored verbatim, AreEqual("CustomFilename", …) fails). 2026-08-02 public void ScriptFilename_SetterNormalizesAndPersists() { AddressableIdsEditorSettings.instance.ScriptFilename = " CustomFilename "; @@ -154,6 +197,10 @@ public void ScriptFilename_SetterNormalizesAndPersists() } [Test] + // ADMIT: AddressableIdsEditorSettings.Namespace's setter trims surrounding whitespace and substitutes the default + // for null. + // RCR: AddressableIdsEditorSettings.cs Namespace setter — drop the `.Trim()` → RED (the padded value is stored + // verbatim). 2026-08-02 public void Namespace_SetterNormalizesAndPersists() { AddressableIdsEditorSettings.instance.Namespace = " Custom.Namespace "; @@ -166,6 +213,10 @@ public void Namespace_SetterNormalizesAndPersists() } [Test] + // ADMIT: AddressableIdsEditorSettings.AddressableLabel's setter trims surrounding whitespace and maps null to the + // empty filter. + // RCR: AddressableIdsEditorSettings.cs AddressableLabel setter — drop the `.Trim()` → RED (" custom-label " is + // stored verbatim). 2026-08-02 public void AddressableLabel_SetterNormalizesAndPersists() { AddressableIdsEditorSettings.instance.AddressableLabel = " custom-label "; diff --git a/Tests/EditMode/Unit/AssetConfigsScriptableObjectTest.cs b/Tests/EditMode/Unit/AssetConfigsScriptableObjectTest.cs index 1321d2f..d933513 100644 --- a/Tests/EditMode/Unit/AssetConfigsScriptableObjectTest.cs +++ b/Tests/EditMode/Unit/AssetConfigsScriptableObjectTest.cs @@ -15,6 +15,10 @@ public class AssetConfigsScriptableObjectTest private class TestAssetConfigs : AssetConfigsScriptableObject { } [Test] + // ADMIT: AssetConfigsScriptableObjectBase.OnAfterDeserialize projects the serialized Configs list into + // ConfigsDictionary. + // RCR: AssetConfigsScriptableObject.cs OnAfterDeserialize — drop the per-pair `dictionary.Add` → RED + // (ConfigsDictionary.Count is 0, not 2). 2026-08-02 public void OnAfterDeserialize_RebuildsDictionaryFromConfigs() { var so = ScriptableObject.CreateInstance(); diff --git a/Tests/EditMode/Unit/AssetLoaderUtilsTest.cs b/Tests/EditMode/Unit/AssetLoaderUtilsTest.cs index 0a8c23d..af61ef7 100644 --- a/Tests/EditMode/Unit/AssetLoaderUtilsTest.cs +++ b/Tests/EditMode/Unit/AssetLoaderUtilsTest.cs @@ -19,6 +19,10 @@ public void Interleaved_EmptyInput_ReturnsEmpty() } [Test] + // ADMIT: AssetLoaderUtils.Interleaved fills the next free bucket per completion via the interlocked counter, so + // bucket N resolves to the Nth task to finish. + // RCR: AssetLoaderUtils.cs Interleaved.Continuation — always target bucket 0 → RED (buckets[1] and buckets[2] + // never complete). 2026-08-02 public void Interleaved_CompletesInCompletionOrder() { var tcs1 = new TaskCompletionSource(); diff --git a/Tests/EditMode/Unit/AssetResolverServiceTest.cs b/Tests/EditMode/Unit/AssetResolverServiceTest.cs index cdf7a11..29d983a 100644 --- a/Tests/EditMode/Unit/AssetResolverServiceTest.cs +++ b/Tests/EditMode/Unit/AssetResolverServiceTest.cs @@ -53,6 +53,10 @@ public void AddAssets_DuplicateType_MergesEntries() } [Test] + // ADMIT: AssetResolverService.UnloadAssets(bool) warns and returns instead of throwing when the asset + // type was never registered. + // RCR: AssetResolverService.cs UnloadAssets(bool) — suppress the Debug.LogWarning → RED (LogAssert.Expect(Warning) + // is unmet). Also reddens UnloadAssets_ClearReferences_RemovesMap. 2026-08-02 public void UnloadAssets_UnknownType_DoesNotThrow() { LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex(".*")); @@ -60,6 +64,9 @@ public void UnloadAssets_UnknownType_DoesNotThrow() } [Test] + // ADMIT: AssetResolverService.UnloadAssets(bool) drops the id-map entry when clearReferences is set. + // RCR: AssetResolverService.cs UnloadAssets(bool) — skip the map removal → RED (the follow-up call still resolves + // the map, so the expected warning never fires). 2026-08-02 public void UnloadAssets_ClearReferences_RemovesMap() { var assetRef = new AssetReference(); @@ -94,6 +101,9 @@ public void SelectAsset_WhenReferenceNotDone_ReturnsErrorPlaceholderNotNull() } [Test] + // ADMIT: AssetResolverService.AddDebugConfigs stores the error Material used by SelectAsset's not-loaded fallback. + // RCR: AssetResolverService.cs AddDebugConfigs — null the `_errorMaterial` assignment → RED (the reflected field + // is null, AreSame fails). 2026-08-02 public void AddDebugConfigs_StoresAllProvided() { var shader = Shader.Find("Standard") ?? Shader.Find("Unlit/Color"); @@ -175,6 +185,10 @@ public async System.Threading.Tasks.Task RequestAsset_UnknownId_ThrowsMissingMem } [Test] + // ADMIT: AssetResolverService.LoadSceneAsync throws MissingMemberException when no scene AssetReference was + // registered for the id. + // RCR: AssetResolverService.cs LoadSceneAsync — return default instead of throwing → RED (no + // MissingMemberException is caught). 2026-08-02 public async System.Threading.Tasks.Task LoadSceneAsync_UnknownId_ThrowsMissingMember() { MissingMemberException caught = null; @@ -191,6 +205,11 @@ public async System.Threading.Tasks.Task LoadSceneAsync_UnknownId_ThrowsMissingM } [Test] + // ADMIT: AssetResolverService.RequestAsset throws MissingMemberException when no AssetReference + // was registered for the id. + // RCR: AssetResolverService.cs RequestAsset — return null instead of throwing → RED (no + // MissingMemberException is caught). Also reddens the two-parameter overload's test, which delegates here. + // 2026-08-02 public async System.Threading.Tasks.Task RequestAsset_ThreeParamWithData_UnknownId_ThrowsMissingMember() { MissingMemberException caught = null; @@ -207,6 +226,10 @@ public async System.Threading.Tasks.Task RequestAsset_ThreeParamWithData_Unknown } [Test] + // ADMIT: AssetResolverService.LoadAllAssets throws MissingMemberException when the asset type was + // never registered. + // RCR: AssetResolverService.cs LoadAllAssets — return the empty list instead of throwing → RED (no + // MissingMemberException is caught). 2026-08-02 public async System.Threading.Tasks.Task LoadAllAssets_UnknownAssetType_ThrowsMissingMember() { MissingMemberException caught = null; @@ -223,6 +246,10 @@ public async System.Threading.Tasks.Task LoadAllAssets_UnknownAssetType_ThrowsMi } [Test] + // ADMIT: AssetResolverService.UnloadSceneAsync logs a warning and completes rather than throwing for an + // unregistered scene id. + // RCR: AssetResolverService.cs UnloadSceneAsync — suppress the Debug.LogWarning → RED + // (LogAssert.Expect(Warning) is unmet). 2026-08-02 public async System.Threading.Tasks.Task UnloadSceneAsync_UnknownId_LogsWarningAndCompletes() { LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex(".*")); diff --git a/Tests/EditMode/Unit/AssetsImporterEditorSettingsTest.cs b/Tests/EditMode/Unit/AssetsImporterEditorSettingsTest.cs index eb399f4..2f66218 100644 --- a/Tests/EditMode/Unit/AssetsImporterEditorSettingsTest.cs +++ b/Tests/EditMode/Unit/AssetsImporterEditorSettingsTest.cs @@ -23,6 +23,10 @@ public void Cleanup() } [Test] + // ADMIT: AssetsImporterEditorSettings.AutoUpdateOnRefresh's setter stores the caller's value on the backing field + // before persisting. + // RCR: AssetsImporterEditorSettings.cs AutoUpdateOnRefresh setter — hard-code the stored value to false → RED (the + // getter never reports true). 2026-08-02 public void AutoUpdateOnRefresh_SetterRoundTrips_PreservesValue() { AssetsImporterEditorSettings.instance.AutoUpdateOnRefresh = true; diff --git a/Tests/EditMode/Unit/CommandServiceTest.cs b/Tests/EditMode/Unit/CommandServiceTest.cs index d54e637..ebdc510 100644 --- a/Tests/EditMode/Unit/CommandServiceTest.cs +++ b/Tests/EditMode/Unit/CommandServiceTest.cs @@ -47,6 +47,10 @@ public void Init() } [Test] + // ADMIT: CommandService.ExecuteCommand invokes the command with the injected game logic and message + // broker. + // RCR: CommandService.cs ExecuteCommand — drop the `command.Execute(...)` call → RED (Received().CallMockup(1) is + // never satisfied). 2026-08-02 public void ExecuteCommand_Successfully() { var payload = 1; diff --git a/Tests/EditMode/Unit/DataServiceTest.cs b/Tests/EditMode/Unit/DataServiceTest.cs index a994f22..f548c5c 100644 --- a/Tests/EditMode/Unit/DataServiceTest.cs +++ b/Tests/EditMode/Unit/DataServiceTest.cs @@ -59,6 +59,9 @@ public void Cleanup() } [Test] + // ADMIT: DataService.AddOrReplaceData stores the caller's instance under typeof(T) on the add path. + // RCR: DataService.cs AddOrReplaceData — store null in the add branch → RED (GetData returns null, AreSame fails). + // Also reddens HasData_Successfully and the save/load round trips. 2026-08-02 public void AddData_Successfully() { var data = Substitute.For(); @@ -82,12 +85,20 @@ public void ReplaceData_Successfully() } [Test] + // ADMIT: DataService.GetData indexes the dictionary directly, so a missing type surfaces as KeyNotFoundException + // rather than a silent null. + // RCR: DataService.cs GetData — swap the indexer for a TryGetValue-with-null fallback → RED (no + // KeyNotFoundException is thrown). 2026-08-02 public void GetData_NotFound_ThrowsException() { Assert.Throws(() => _dataService.GetData()); } [Test] + // ADMIT: DataService.SaveData serialises the in-memory entry to PlayerPrefs under typeof(T).Name, which is what + // LoadData reads back. + // RCR: DataService.cs SaveData — drop the PlayerPrefs write → RED (the second service loads a fresh instance + // with Name null). 2026-08-02 public void SaveData_LoadData_RoundTrip_Successfully() { var data = new PersistentData { Name = "Test", Value = 123 }; @@ -102,6 +113,10 @@ public void SaveData_LoadData_RoundTrip_Successfully() } [Test] + // ADMIT: DataService.LoadData falls back to Activator.CreateInstance() when PlayerPrefs holds no JSON for + // the type. + // RCR: DataService.cs LoadData — always deserialize → RED (DeserializeObject on an empty string returns null, + // Assert.IsNotNull fails). 2026-08-02 public void LoadData_NoExistingData_CreatesNew() { var loadedData = _dataService.LoadData(); @@ -112,6 +127,9 @@ public void LoadData_NoExistingData_CreatesNew() } [Test] + // ADMIT: DataService.HasData reports whether the in-memory store holds an entry for the type. + // RCR: DataService.cs HasData — always return false → RED (Assert.IsTrue fails). Also reddens + // SaveAllData_Successfully, whose second AddOrReplaceData then hits Dictionary.Add twice. 2026-08-02 public void HasData_Successfully() { var data = new PersistentData(); @@ -122,12 +140,18 @@ public void HasData_Successfully() } [Test] + // ADMIT: DataService.HasData reports false for a type that was never added. + // RCR: DataService.cs HasData — always return true → RED (Assert.IsFalse fails). 2026-08-02 public void HasData_NotFound_ReturnsFalse() { Assert.IsFalse(_dataService.HasData()); } [Test] + // ADMIT: DataService.SaveAllData rewrites every in-memory entry, overwriting the earlier single-key SaveData + // snapshot. + // RCR: DataService.cs SaveAllData — drop the PlayerPrefs write → RED (the reload returns the stale 'Hero' + // snapshot, not 'Alt'). 2026-08-02 public void SaveAllData_Successfully() { var data1 = new PersistentData { Name = "Hero", Value = 10 }; @@ -147,6 +171,8 @@ public void SaveAllData_Successfully() } [Test] + // ADMIT: DataService.SaveData invokes the protected OnDataSaved hook with the key, instance and type. + // RCR: DataService.cs SaveData — drop the OnDataSaved call → RED (subclass.SaveCount stays 0). 2026-08-02 public void OnDataSaved_SubclassHook_FiresAfterSave() { var subclass = new TestableDataService(); diff --git a/Tests/EditMode/Unit/InstallerTest.cs b/Tests/EditMode/Unit/InstallerTest.cs index 17c5f48..e0894ec 100644 --- a/Tests/EditMode/Unit/InstallerTest.cs +++ b/Tests/EditMode/Unit/InstallerTest.cs @@ -24,6 +24,9 @@ public void Init() } [Test] + // ADMIT: Installer.Resolve casts and returns the bound instance for the requested interface. + // RCR: Installer.cs Resolve — return default(T) instead of the bound instance → RED (Assert.IsNotNull fails). Also + // reddens the multi-interface and Clean tests. 2026-08-02 public void Bind_Resolve_Successfully() { _installer.Bind(new Implementation()); @@ -35,18 +38,27 @@ public void Bind_Resolve_Successfully() } [Test] + // ADMIT: Installer.Bind rejects a non-interface type parameter because the registry is keyed by interface. + // RCR: Installer.cs Bind — return `this` instead of throwing on a non-interface → RED (no ArgumentException). + // 2026-08-02 public void Bind_NotInterface_ThrowsException() { Assert.Throws(() => _installer.Bind(new Implementation())); } [Test] + // ADMIT: Installer.Resolve throws ArgumentException for an unbound interface rather than returning null. + // RCR: Installer.cs Resolve — return default(T) from the missing-binding branch → RED (no ArgumentException). Also + // reddens Clean_Generic_RemovesOnlyBoundInterface. 2026-08-02 public void Resolve_NotBinded_ThrowsException() { Assert.Throws(() => _installer.Resolve()); } [Test] + // ADMIT: Installer.Bind binds the instance under the second interface as well as the first. + // RCR: Installer.cs Bind — bind null for T2 → RED (Resolve() returns null, AreSame fails). + // 2026-08-02 public void Bind_MultiInterface_ResolveBothInterfaces() { var instance = new MultiImpl(); @@ -57,6 +69,9 @@ public void Bind_MultiInterface_ResolveBothInterfaces() } [Test] + // ADMIT: Installer.Bind binds the instance under the third interface as well. + // RCR: Installer.cs Bind — bind null for T3 → RED (Resolve() returns null, AreSame + // fails). 2026-08-02 public void Bind_TripleInterface_ResolveAllInterfaces() { var instance = new TripleImpl(); @@ -68,6 +83,9 @@ public void Bind_TripleInterface_ResolveAllInterfaces() } [Test] + // ADMIT: Installer.TryResolve outs the bound instance, not just the found/not-found flag. + // RCR: Installer.cs TryResolve — out default(T) instead of the cast instance → RED (AreSame(instance, bound) fails + // while the bool assertions still pass). 2026-08-02 public void TryResolve_DirectInvocation_OutsValueWhenBound() { var instance = new Implementation(); @@ -83,6 +101,9 @@ public void TryResolve_DirectInvocation_OutsValueWhenBound() } [Test] + // ADMIT: Installer.Clean removes the binding for exactly the requested interface. + // RCR: Installer.cs Clean — skip the removal → RED (Resolve() still succeeds, Assert.Throws fails). + // 2026-08-02 public void Clean_Generic_RemovesOnlyBoundInterface() { var first = new Implementation(); diff --git a/Tests/EditMode/Unit/MainInstallerTest.cs b/Tests/EditMode/Unit/MainInstallerTest.cs index d1fa2ad..be1533d 100644 --- a/Tests/EditMode/Unit/MainInstallerTest.cs +++ b/Tests/EditMode/Unit/MainInstallerTest.cs @@ -25,6 +25,9 @@ public void Cleanup() } [Test] + // ADMIT: MainInstaller.Resolve delegates to the private static Installer so a bound instance comes back out. + // RCR: MainInstaller.cs Resolve — return default(T) instead of delegating → RED (AreSame fails). Also reddens + // the PlayMode bind/resolve tests. 2026-08-02 public void Bind_Resolve_Successfully() { var implementation = new Implementation(); @@ -34,6 +37,9 @@ public void Bind_Resolve_Successfully() } [Test] + // ADMIT: MainInstaller.Clean() clears every binding on the private static Installer. + // RCR: MainInstaller.cs Clean() — drop the delegation → RED (TryResolve still finds the binding). Note: this also + // disables the fixture's TearDown cleanup. 2026-08-02 public void Clean_RemovesAllBindings() { MainInstaller.Bind(new Implementation()); @@ -43,6 +49,8 @@ public void Clean_RemovesAllBindings() } [Test] + // ADMIT: MainInstaller.Clean() delegates the single-interface removal to the private static Installer. + // RCR: MainInstaller.cs Clean() — skip the delegation → RED (TryResolve still finds the binding). 2026-08-02 public void CleanGeneric_RemovesSpecificBinding() { MainInstaller.Bind(new Implementation()); @@ -52,6 +60,9 @@ public void CleanGeneric_RemovesSpecificBinding() } [Test] + // ADMIT: MainInstaller.CleanDispose disposes the bound instance before removing the binding. + // RCR: MainInstaller.cs CleanDispose — drop the Resolve().Dispose() call → RED (Received(1).Dispose() is never + // satisfied). 2026-08-02 public void CleanDispose_CallsDispose() { var disposable = Substitute.For(); @@ -64,6 +75,10 @@ public void CleanDispose_CallsDispose() } [Test] + // ADMIT: MainInstaller.TryResolve forwards to the wrapped Installer rather than reporting success on its own. + // RCR: MainInstaller.cs TryResolve — `instance = default; return true;` → RED (returns true with nothing bound). + // The bare `return true;` is NOT a usable mutation: it leaves the `out` parameter unassigned and fails to + // compile, which the harness reports as a missing report rather than a red. public void TryResolve_NotBound_ReturnsFalse() { Assert.IsFalse(MainInstaller.TryResolve(out _)); diff --git a/Tests/EditMode/Unit/MessageBrokerServiceTest.cs b/Tests/EditMode/Unit/MessageBrokerServiceTest.cs index 5949596..d50be23 100644 --- a/Tests/EditMode/Unit/MessageBrokerServiceTest.cs +++ b/Tests/EditMode/Unit/MessageBrokerServiceTest.cs @@ -35,6 +35,9 @@ public void Init() } [Test] + // ADMIT: MessageBrokerService.Publish invokes every stored delegate for the message type. + // RCR: MessageBrokerService.cs Publish — drop the `action(message)` invocation → RED (Received(2) sees only the + // PublishSafe call). Broad: also reddens the other Received(n) tests. 2026-08-02 public void Subscribe_Publish_Successfully() { _messageBroker.Subscribe(_subscriber.MockMessageCall); @@ -45,6 +48,10 @@ public void Subscribe_Publish_Successfully() } [Test] + // ADMIT: MessageBrokerService.Subscribe keys by action.Target and overwrites, so a second subscription from the + // same object replaces the first. + // RCR: MessageBrokerService.cs Subscribe — only add when the subscriber key is absent → RED (the first handler + // still fires, DidNotReceive fails). 2026-08-02 public void Subscribe_MultipleSubscriptionSameType_ReplacePreviousSubscription() { _messageBroker.Subscribe(_subscriber.MockMessageCall); @@ -57,6 +64,9 @@ public void Subscribe_MultipleSubscriptionSameType_ReplacePreviousSubscription() } [Test] + // ADMIT: MessageBrokerService.Publish raises the _isPublishing flag that makes a re-entrant Subscribe throw. + // RCR: MessageBrokerService.cs Publish — set the publishing flag to false → RED (the chained Subscribe no longer + // throws InvalidOperationException). 2026-08-02 public void Publish_ChainSubscribe_ThrowsException() { _messageBroker.Subscribe(m => _messageBroker.Subscribe(_subscriber.MockMessageAlternativeCall)); @@ -65,6 +75,10 @@ public void Publish_ChainSubscribe_ThrowsException() } [Test] + // ADMIT: MessageBrokerService.PublishSafe dispatches over the copied delegate array, which is what lets the + // handler subscribe mid-publish. + // RCR: MessageBrokerService.cs PublishSafe — drop the `action(message)` invocation → RED (the chained MessageType2 + // subscription never happens, Received(1) fails). 2026-08-02 public void PublishSafe_ChainSubscribe_Succeeds() { _messageBroker.Subscribe(m => _messageBroker.Subscribe(_subscriber.MockMessageAlternativeCall)); @@ -76,6 +90,10 @@ public void PublishSafe_ChainSubscribe_Succeeds() } [Test] + // ADMIT: MessageBrokerService.Subscribe rejects a static method because action.Target is null and cannot key the + // subscription map. + // RCR: MessageBrokerService.cs Subscribe — return instead of throwing on a null Target → RED (no + // ArgumentException). 2026-08-02 public void Subscribe_StaticMethod_ThrowsException() { // The current implementation uses action.Target as the key. @@ -88,6 +106,9 @@ public void Subscribe_StaticMethod_ThrowsException() private static void StaticMockCall(MessageType1 message) {} [Test] + // ADMIT: MessageBrokerService.Publish early-returns when no subscription bucket exists for the message type. + // RCR: MessageBrokerService.cs Publish — drop the early return from the missing-bucket guard → RED + // (NullReferenceException iterating a null bucket, DoesNotThrow fails). 2026-08-02 public void Publish_NoSubscribers_DoesNotThrow() { Assert.DoesNotThrow(() => _messageBroker.Publish(_messageType1)); @@ -95,6 +116,10 @@ public void Publish_NoSubscribers_DoesNotThrow() } [Test] + // ADMIT: MessageBrokerService.Unsubscribe(subscriber) removes that subscriber's delegate from the type's + // bucket. + // RCR: MessageBrokerService.cs Unsubscribe — drop the per-subscriber removal → RED (the handler still fires + // after unsubscribe). 2026-08-02 public void Unsubscribe_Successfully() { _messageBroker.Subscribe(_subscriber.MockMessageCall); @@ -119,6 +144,10 @@ public void UnsubscribeWithAction_MultipleSubscriptionSameType_RemoveAllScriptio } [Test] + // ADMIT: MessageBrokerService.Unsubscribe(null) drops only the requested message type's bucket, leaving other + // types subscribed. + // RCR: MessageBrokerService.cs Unsubscribe — clear the whole subscription map instead of removing one type → + // RED (MessageType2 handler stops receiving). 2026-08-02 public void UnsubscribeWithoutAction_KeepsSubscriptionDifferentType_Successfully() { _messageBroker.Subscribe(_subscriber.MockMessageCall); @@ -132,6 +161,9 @@ public void UnsubscribeWithoutAction_KeepsSubscriptionDifferentType_Successfully } [Test] + // ADMIT: MessageBrokerService.UnsubscribeAll(null) clears every subscription bucket. + // RCR: MessageBrokerService.cs UnsubscribeAll — drop the `_subscriptions.Clear()` call → RED (all four handlers + // still receive). 2026-08-02 public void UnsubscribeAll_Successfully() { _messageBroker.Subscribe(_subscriber.MockMessageCall); @@ -151,6 +183,9 @@ public void UnsubscribeAll_Successfully() } [Test] + // ADMIT: MessageBrokerService.Unsubscribe(subscriber) early-returns when the message type has no bucket at all. + // RCR: MessageBrokerService.cs Unsubscribe — drop the early return from the missing-bucket guard → RED + // (NullReferenceException, DoesNotThrow fails). 2026-08-02 public void Unsubscribe_WithoutSubscription_DoesNothing() { Assert.DoesNotThrow(() => _messageBroker.Unsubscribe(_subscriber)); @@ -159,6 +194,10 @@ public void Unsubscribe_WithoutSubscription_DoesNothing() } [Test] + // ADMIT: MessageBrokerService.UnsubscribeAll(subscriber) removes that subscriber from every type bucket while + // leaving other subscribers intact. + // RCR: MessageBrokerService.cs UnsubscribeAll — drop the per-bucket removal → RED (subA still receives both + // messages). 2026-08-02 public void UnsubscribeAll_NonNullSubscriber_RemovesOnlyMatching() { var subA = Substitute.For(); diff --git a/Tests/EditMode/Unit/ObjectPoolTest.cs b/Tests/EditMode/Unit/ObjectPoolTest.cs index 7fb71c3..75334a2 100644 --- a/Tests/EditMode/Unit/ObjectPoolTest.cs +++ b/Tests/EditMode/Unit/ObjectPoolTest.cs @@ -45,6 +45,9 @@ public void Init() } [Test] + // ADMIT: ObjectPoolBase.Spawn() runs the IPoolEntitySpawn lifecycle hook on the entity it hands out. + // RCR: ObjectPool.cs Spawn() — drop the `CallOnSpawned(entity)` call → RED (Received().OnSpawn() is never + // satisfied). Also reddens Spawn_ZeroInitialSize_Successfully. 2026-08-02 public void Spawn_Successfully() { var newEntity = _pool.Spawn(); @@ -55,6 +58,10 @@ public void Spawn_Successfully() } [Test] + // ADMIT: ObjectPoolBase.Spawn runs the typed IPoolEntitySpawn hook in addition to the untyped + // one. + // RCR: ObjectPool.cs Spawn — drop the `CallOnSpawned(entity, data)` call → RED (Received().OnSpawn(obj) is + // never satisfied). 2026-08-02 public void Spawn_WithData_Successfully() { var obj = new object(); @@ -66,6 +73,10 @@ public void Spawn_WithData_Successfully() } [Test] + // ADMIT: ObjectPoolBase.SpawnEntity instantiates a fresh entity when the free stack is empty instead of popping + // it. + // RCR: ObjectPool.cs SpawnEntity — pop unconditionally → RED (InvalidOperationException 'Stack empty' on a zero- + // init pool). Also reddens the other zero-init fixtures in this file. 2026-08-02 public void Spawn_ZeroInitialSize_Successfully() { var pool = new ObjectPool(0, () => _mockEntity); @@ -77,6 +88,9 @@ public void Spawn_ZeroInitialSize_Successfully() } [Test] + // ADMIT: ObjectPoolBase.Despawn runs the IPoolEntityDespawn hook on a successfully returned entity. + // RCR: ObjectPool.cs Despawn(T) — drop the `CallOnDespawned(entity)` call → RED (Received().OnDespawn() is never + // satisfied). Also reddens DespawnAll_Successfully. 2026-08-02 public void Despawn_Successfully() { _pool.Spawn(); @@ -122,6 +136,9 @@ public void Spawn_OnPoolEntityObject_CallsInitWithOwningPoolEveryTime() } [Test] + // ADMIT: ObjectPoolBase.Despawn reports false for an entity that was never spawned from this pool. + // RCR: ObjectPool.cs Despawn(T) — return true from the reject branch → RED (Assert.IsFalse fails). Note: deleting + // the `SpawnedEntities.Remove` term instead hangs Despawn(bool, Func). 2026-08-02 public void Despawn_NotSpawnedObject_ReturnsFalse() { Assert.IsFalse(_pool.Despawn(_mockEntity)); @@ -129,6 +146,9 @@ public void Despawn_NotSpawnedObject_ReturnsFalse() } [Test] + // ADMIT: ObjectPoolBase.DespawnAll actually despawns each tracked entity rather than just walking the list. + // RCR: ObjectPool.cs DespawnAll — drop the `Despawn(SpawnedEntities[i])` call → RED (neither entity receives + // OnDespawn). 2026-08-02 public void DespawnAll_Successfully() { var newEntity1 = _pool.Spawn(); @@ -141,6 +161,9 @@ public void DespawnAll_Successfully() } [Test] + // ADMIT: ObjectPoolBase.Clear returns the pre-instantiated free-stack entities as well as the spawned ones. + // RCR: ObjectPool.cs Clear — drop the `ret.AddRange(_stack)` call → RED (0 returned instead of the 5 pre- + // instantiated entities). 2026-08-02 public void Clear_Successfully() { var clearedEntities = _pool.Clear(); @@ -149,12 +172,18 @@ public void Clear_Successfully() } [Test] + // ADMIT: ObjectPoolBase's constructor stores the sample entity that SampleEntity exposes. + // RCR: ObjectPool.cs ObjectPoolBase(uint, T, Func) — null the `_sampleEntity` assignment → RED (SampleEntity is + // null, not the mock). 2026-08-02 public void SampleEntity_ReturnsSampleEntity() { Assert.AreSame(_mockEntity, _pool.SampleEntity); } [Test] + // ADMIT: ObjectPoolBase.SpawnedReadOnly exposes the live SpawnedEntities backing list, not a detached copy. + // RCR: ObjectPool.cs SpawnedReadOnly — return a fresh empty list → RED (count 0 and no element to compare). Also + // reddens the other SpawnedReadOnly-count assertions in this fixture. 2026-08-02 public void SpawnedReadOnly_ReturnsSpawnedEntities() { var entity = _pool.Spawn(); @@ -166,6 +195,10 @@ public void SpawnedReadOnly_ReturnsSpawnedEntities() } [Test] + // ADMIT: ObjectPoolBase.IsSpawned returns true for the first spawned entity satisfying the predicate and false + // otherwise. + // RCR: ObjectPool.cs IsSpawned — invert the predicate test → RED (the matching probe returns false and the always- + // false probe returns true). 2026-08-02 public void IsSpawned_ReturnsTrueWhenMatch() { var entity = _pool.Spawn(); @@ -184,6 +217,9 @@ public void Despawn_WithCondition_FirstOnly_Successfully() } [Test] + // ADMIT: ObjectPoolBase.Despawn(bool, Func) reports false when the predicate matched nothing. + // RCR: ObjectPool.cs Despawn(bool, Func) — seed the result accumulator to true → RED (Assert.IsFalse fails while + // the entity correctly survives). 2026-08-02 public void Despawn_WithCondition_NoMatch_ReturnsFalse() { _pool.Spawn(); @@ -203,6 +239,10 @@ public void Despawn_WithCondition_AllMatching_DespawnsAll() } [Test] + // ADMIT: ObjectPoolBase.Despawn(bool, Func) steps the index back after a removal so adjacent distinct matches + // are not skipped. + // RCR: ObjectPool.cs Despawn(bool, Func) — delete the `i--` step-back → RED (only the first of two distinct + // entities is despawned). Also reddens Despawn_WithCondition_AllMatching_DespawnsAll. 2026-08-02 public void Despawn_WithCondition_DistinctMatchingEntities_AllDespawn() { // Regression: Despawn_WithCondition_AllMatching_DespawnsAll spawns the same _mockEntity @@ -220,6 +260,10 @@ public void Despawn_WithCondition_DistinctMatchingEntities_AllDespawn() } [Test] + // ADMIT: ObjectPoolBase.Despawn(bool, Func) skips predicate-rejected entities so non-matching neighbours + // survive the step-back walk. + // RCR: ObjectPool.cs Despawn(bool, Func) — delete the predicate skip → RED (the keeper is despawned too, count 0). + // Also reddens Despawn_WithCondition_NoMatch_ReturnsFalse. 2026-08-02 public void Despawn_WithCondition_PartialMatch_NonMatchingSurvives() { // Confirms the iteration step-back after a successful despawn doesn't spuriously remove @@ -234,6 +278,9 @@ public void Despawn_WithCondition_PartialMatch_NonMatchingSurvives() } [Test] + // ADMIT: ObjectPoolBase.Reset re-seeds _sampleEntity with the new sample after disposing the old contents. + // RCR: ObjectPool.cs Reset — drop the `_sampleEntity = sampleEntity` assignment → RED (SampleEntity still points + // at the original mock). 2026-08-02 public void Reset_ClearsAndReinitializes() { _pool.Spawn(); @@ -247,6 +294,10 @@ public void Reset_ClearsAndReinitializes() } [Test] + // ADMIT: ObjectPoolBase.SpawnEntity reuses the pre-instantiated free stack, so the Func-only ctor's factory is + // invoked exactly initSize + 1 times. + // RCR: ObjectPool.cs SpawnEntity — always instantiate instead of popping → RED (invocations is 7, not 4, after + // three spawns). 2026-08-02 public void ObjectPool_FuncOnlyCtor_UsesProvidedFactory() { var invocations = 0; diff --git a/Tests/EditMode/Unit/PoolServiceTest.cs b/Tests/EditMode/Unit/PoolServiceTest.cs index c988f18..ba1fc60 100644 --- a/Tests/EditMode/Unit/PoolServiceTest.cs +++ b/Tests/EditMode/Unit/PoolServiceTest.cs @@ -69,6 +69,9 @@ public void Dispose() } [Test] + // ADMIT: PoolService.TryGetPool casts the stored IObjectPool to IObjectPool and outs it. + // RCR: PoolService.cs TryGetPool — out null instead of the cast pool → RED (AreEqual(_pool, pool) fails). Also + // reddens GetPool_Successfully. 2026-08-02 public void TryGetPool_Successfully() { Assert.True(_poolService.TryGetPool(out var pool)); @@ -76,6 +79,9 @@ public void TryGetPool_Successfully() } [Test] + // ADMIT: PoolService.GetPool returns the pool TryGetPool resolved for the requested type. + // RCR: PoolService.cs GetPool — return null after the guard → RED (AreEqual(_pool, …) fails). Other Spawn/Despawn + // tests fail by NRE under this mutation. 2026-08-02 public void GetPool_Successfully() { Assert.AreEqual(_pool, _poolService.GetPool()); @@ -88,12 +94,19 @@ public void AddPool_Successfully() } [Test] + // ADMIT: PoolService.AddPool uses Dictionary.Add, so registering a second pool for the same type throws instead of + // silently replacing. + // RCR: PoolService.cs AddPool — switch to the indexer assignment → RED (no ArgumentException is thrown). + // 2026-08-02 public void AddPool_SameType_ThrowsException() { Assert.Throws(() => _poolService.AddPool(_pool)); } [Test] + // ADMIT: PoolService.Spawn delegates to the registered pool and returns its instance. + // RCR: PoolService.cs Spawn — return null instead of delegating → RED (Assert.IsNotNull(entity) fails). + // 2026-08-02 public void Spawn_Successfully() { var entity = _poolService.Spawn(); @@ -103,6 +116,9 @@ public void Spawn_Successfully() } [Test] + // ADMIT: PoolService.GetPool throws ArgumentException when no pool is registered for the requested type. + // RCR: PoolService.cs GetPool — return null instead of throwing → RED (NullReferenceException, not + // ArgumentException). Also reddens Despawn_NotAddedPool and RemovePool_Successfully. 2026-08-02 public void Spawn_NotAddedPool_ThrowsException() { _poolService = new PoolService(); @@ -119,6 +135,10 @@ public void Despawn_Successfully() } [Test] + // ADMIT: PoolService.Despawn routes through GetPool, so despawning into an unregistered type surfaces GetPool's + // ArgumentException. + // RCR: PoolService.cs Despawn — return false without resolving the pool → RED (no ArgumentException is thrown). + // 2026-08-02 public void Despawn_NotAddedPool_ThrowsException() { var entity = new MockPoolableEntity(); @@ -138,6 +158,9 @@ public void DespawnAll_Successfully() } [Test] + // ADMIT: PoolService.RemovePool deletes the registry entry so a later GetPool throws. + // RCR: PoolService.cs RemovePool — drop the dictionary removal → RED (GetPool still resolves, no + // ArgumentException). Also reddens Dispose_RemovesAndDisposesPool. 2026-08-02 public void RemovePool_Successfully() { _poolService.RemovePool(); @@ -154,6 +177,9 @@ public void RemovePool_NotAdded_DoesNothing() } [Test] + // ADMIT: PoolService.Spawn forwards the payload to the pool's data-aware Spawn overload. + // RCR: PoolService.cs Spawn — call the parameterless Spawn instead → RED (SpawnData stays 0, not 42). + // 2026-08-02 public void SpawnWithData_Successfully() { var dataPool = new ObjectPool(0, () => new MockDataEntity()); @@ -166,6 +192,9 @@ public void SpawnWithData_Successfully() } [Test] + // ADMIT: PoolService.Clear returns a snapshot of the registry it is about to empty. + // RCR: PoolService.cs Clear — return an empty dictionary instead of a copy of _pools → RED (cleared.Count is 0, + // not 1). 2026-08-02 public void Clear_ReturnsAllPools() { IDictionary cleared = _poolService.Clear(); @@ -176,6 +205,9 @@ public void Clear_ReturnsAllPools() } [Test] + // ADMIT: PoolService.Dispose(bool) unregisters the pool after disposing it. + // RCR: PoolService.cs Dispose(bool) — drop the `RemovePool()` call → RED (TryGetPool still resolves the + // disposed pool). 2026-08-02 public void Dispose_RemovesAndDisposesPool() { _poolService.Dispose(disposeSampleEntity: false); @@ -184,6 +216,9 @@ public void Dispose_RemovesAndDisposesPool() } [Test] + // ADMIT: PoolService.Dispose() disposes every registered pool before clearing the registry. + // RCR: PoolService.cs Dispose() — drop the per-pool `Dispose()` call → RED (both fakes report DisposeCount 0). + // 2026-08-02 public void Dispose_DisposesAllRegisteredPools() { var fakeA = new FakeObjectPool(); diff --git a/Tests/EditMode/Unit/RngServiceTest.cs b/Tests/EditMode/Unit/RngServiceTest.cs index c136d9e..bae37e9 100644 --- a/Tests/EditMode/Unit/RngServiceTest.cs +++ b/Tests/EditMode/Unit/RngServiceTest.cs @@ -36,6 +36,10 @@ public void Next_SameSeed_ReturnsDeterministicSequence() } [Test] + // ADMIT: RngService.PeekRange(int,int,bool) draws from a copy of the state so Peek never advances the live + // sequence. + // RCR: RngService.cs PeekRange(int,int,bool) — pass the live `_rngData.State` instead of a copy → RED (the two + // Peek reads differ). Also reddens PeekRange_IntBounds. 2026-08-02 public void Peek_DoesNotAdvanceState() { var peeked = _rngService.Peek; @@ -48,6 +52,10 @@ public void Peek_DoesNotAdvanceState() } [Test] + // ADMIT: RngService.Range's degenerate-range early return yields exactly min when the bounds are within + // floatP.Epsilon. + // RCR: RngService.cs Range(floatP,floatP,int[],bool) — return `min + 1` from the equal-bounds early return → RED + // (11 instead of 10). Also reddens the annotated float inclusive sibling. 2026-08-02 public void Range_MinEqualsMax_ReturnsMin() { const int minMax = 10; @@ -55,6 +63,10 @@ public void Range_MinEqualsMax_ReturnsMin() } [Test] + // ADMIT: RngService.Range(int,int,int[],bool) widens both int bounds to floatP before the inverted-range guard + // runs, so Range(10, 5) reaches the throw. + // RCR: RngService.cs Range(int,int,int[],bool) — widen min as 0 instead → RED (bounds become 0..5, the guard never + // fires and no IndexOutOfRangeException is thrown). 2026-08-02 public void Range_MinGreaterThanMax_ThrowsException() { Assert.Throws(() => _rngService.Range(10, 5)); @@ -89,6 +101,10 @@ public void Range_FloatMinEqualsMaxWithMaxExclusive_ThrowsIndexOutOfRange() } [Test] + // ADMIT: RngService.Restore(int) rebuilds the state array from the seed so the sequence replays from the restored + // count. + // RCR: RngService.cs Restore(int) — drop the state rebuild → RED (the next value after Restore is not the + // previously peeked value). 2026-08-02 public void Restore_ToPastCount_ReproducesSequence() { _ = _rngService.Next; @@ -106,6 +122,10 @@ public void Restore_ToPastCount_ReproducesSequence() } [Test] + // ADMIT: RngService.Restore(int) writes the requested count back onto RngData, including counts ahead of the + // current one. + // RCR: RngService.cs Restore(int) — zero the count assignment → RED (Counter is 0, not 5). Also reddens + // Restore_ToPastCount's Counter assertion. 2026-08-02 public void Restore_ToFutureCount_AdvancesCorrectly() { var count = 5; @@ -115,6 +135,10 @@ public void Restore_ToFutureCount_AdvancesCorrectly() } [Test] + // ADMIT: RngService.CopyRngState allocates a fresh array, so mutating the live state cannot reach a previously + // taken copy. + // RCR: RngService.cs CopyRngState — alias the caller's array instead of allocating → RED (the copy tracks the + // advanced state, AreNotEqual fails). Broad: also reddens every Peek test. 2026-08-02 public void CopyRngState_CreatesIndependentCopy() { var stateCopy = RngService.CopyRngState(_rngData.State); @@ -126,6 +150,9 @@ public void CopyRngState_CreatesIndependentCopy() } [Test] + // ADMIT: RngService.CreateRngData records the caller's seed on the RngData it returns. + // RCR: RngService.cs CreateRngData — store 0 instead of `seed` → RED (data.Seed is 0, not 12345). Also reddens + // Restore_ToPastCount, which rebuilds from the stored seed. 2026-08-02 public void CreateRngData_InitializesCorrectly() { var data = RngService.CreateRngData(Seed); @@ -151,6 +178,11 @@ public void Nextfloat_ReturnsDeterministicSequence() } [Test] + // ADMIT: RngService.Peekfloat draws through PeekRange, which works on a copy of the state, so repeated reads + // return the same value and never advance the live sequence. + // RCR: RngService.cs Peekfloat — narrow the range to `(floatP) 1000f` → RED. NOTE making PeekRange consume the + // LIVE state was observed to stay GREEN here: at `floatP.MaxValue` consecutive draws saturate to the same + // floatP, so this test is precision-blind to the very advance its name claims to catch. public void Peekfloat_DoesNotAdvanceState() { floatP peeked1 = _rngService.Peekfloat; @@ -177,6 +209,10 @@ public void RangeFloat_ReturnsValueInRange() } [Test] + // ADMIT: RngService.Range(floatP,floatP,bool) is the consuming overload and increments RngData.Count; PeekRange + // must not. + // RCR: RngService.cs Range(floatP,floatP,bool) — drop the `_rngData.Count++` → RED (Counter is 0 after the + // consuming Range call). Also reddens Peekfloat_DoesNotAdvanceState's Counter assertion. 2026-08-02 public void PeekRangeFloat_DoesNotAdvanceState() { floatP min = (floatP)0f; @@ -194,6 +230,10 @@ public void PeekRangeFloat_DoesNotAdvanceState() } [Test] + // ADMIT: RngService.Range(int,int,bool) is the consuming overload and increments RngData.Count; PeekRange must + // not. + // RCR: RngService.cs Range(int,int,bool) — drop the `_rngData.Count++` → RED (Counter is 0 after the consuming + // Range call). Also reddens Peek_DoesNotAdvanceState's Counter assertion. 2026-08-02 public void PeekRange_IntBounds_ReturnsValueInRange_DoesNotAdvance() { const int min = 5; @@ -213,6 +253,10 @@ public void PeekRange_IntBounds_ReturnsValueInRange_DoesNotAdvance() } [Test] + // ADMIT: RngService.Range draws from NextNumber, so repeated calls over a wide closed range produce more than one + // distinct value. + // RCR: RngService.cs Range(floatP,floatP,int[],bool) — hard-code the draw to 0 → RED (every sample is min, + // seenValues.Count is 1). Also reddens Range_IntStaticOverload's variance assertion. 2026-08-02 public void Range_IntMaxInclusiveTrue_StaysWithinClosedRangeAndVaries() { const int min = 0; @@ -232,6 +276,9 @@ public void Range_IntMaxInclusiveTrue_StaysWithinClosedRangeAndVaries() } [Test] + // ADMIT: RngService.Range(floatP,floatP,int[],bool) throws for inverted bounds under both maxInclusive settings. + // RCR: RngService.cs Range(floatP,floatP,int[],bool) — return min instead of throwing → RED (neither Assert.Throws + // fires). Also reddens the annotated exclusive-empty-range sibling. 2026-08-02 public void Range_FloatPStaticOverload_InBoundsHappyPath_AndThrowsOnInvertedBounds() { var state = RngService.CopyRngState(_rngData.State); @@ -248,6 +295,9 @@ public void Range_FloatPStaticOverload_InBoundsHappyPath_AndThrowsOnInvertedBoun } [Test] + // ADMIT: RngService.CopyRngState rejects any state array whose length is not the 56-entry Knuth state. + // RCR: RngService.cs CopyRngState — drop the length term from the guard → RED (new int[0]/int[10]/int[55] no + // longer throw). 2026-08-02 public void CopyRngState_WrongLengthInput_Throws() { Assert.Throws(() => RngService.CopyRngState(null)); @@ -257,6 +307,10 @@ public void CopyRngState_WrongLengthInput_Throws() } [Test] + // ADMIT: RngService.Restore(int,int) advances the freshly generated state exactly `count` times so it matches + // count iterative Next calls. + // RCR: RngService.cs Restore(int,int) — advance count - 1 times → RED (staticState differs from the iteratively + // advanced state). Also reddens Restore_ToPastCount. 2026-08-02 public void Restore_StaticOverload_AgreesWithIterativeNextOnFreshSeed() { const int count = 7; @@ -275,6 +329,9 @@ public void Restore_StaticOverload_AgreesWithIterativeNextOnFreshSeed() } [Test] + // ADMIT: RngService.Range offsets the scaled draw by min, keeping results inside [min, max). + // RCR: RngService.cs Range(floatP,floatP,int[],bool) — offset by max instead → RED (values land in [max, + // max+range), Assert.Less fails). Broad: also reddens every other bounded-range test. 2026-08-02 public void Range_IntStaticOverload_StaysWithinBoundsAndAdvancesState() { const int min = -50; diff --git a/Tests/EditMode/Unit/TimeServiceTest.cs b/Tests/EditMode/Unit/TimeServiceTest.cs index 907c6dd..9c6e2d2 100644 --- a/Tests/EditMode/Unit/TimeServiceTest.cs +++ b/Tests/EditMode/Unit/TimeServiceTest.cs @@ -19,6 +19,10 @@ public void Init() } [Test] + // ADMIT: TimeService.DateTimeUtcFromUnixTime treats its argument as milliseconds since the Unix epoch, matching + // UnixTimeNow. + // RCR: TimeService.cs DateTimeUtcFromUnixTime — add the value as seconds instead of milliseconds → RED (the round + // trip is off by ~1000x). Also reddens UnityTime_Convertions. 2026-08-02 public void DateTime_Convertions_Successfully() { Assert.GreaterOrEqual(ErrorValue, (_timeService.DateTimeUtcFromUnityTime(_timeService.UnityTimeNow) - _timeService.DateTimeUtcNow).TotalMilliseconds); @@ -26,6 +30,11 @@ public void DateTime_Convertions_Successfully() } [Test] + // ADMIT: TimeService.UnityTimeFromDateTimeUtc/FromUnixTime rebase onto _initialUnityTime so a converted + // instant lands near UnityTimeNow. + // RCR: TimeService.cs UnityTimeFromDateTimeUtc — add +1000f to the returned offset → RED. NOTE the assertion is + // ONE-SIDED (`GreaterOrEqual(ErrorValue, diff)`), so it only catches conversions that grow: negating + // `_initialUnityTime` instead was observed to stay GREEN. A regression that shrinks the conversion is invisible. public void UnityTime_Convertions_Successfully() { Assert.GreaterOrEqual(ErrorValue, _timeService.UnityTimeFromDateTimeUtc(_timeService.DateTimeUtcNow) - _timeService.UnityTimeNow); @@ -33,6 +42,11 @@ public void UnityTime_Convertions_Successfully() } [Test] + // ADMIT: TimeService.UnixTimeFromDateTimeUtc returns milliseconds since the Unix epoch, the unit UnixTimeNow + // reports in. + // RCR: TimeService.cs UnixTimeFromDateTimeUtc — add +100000L to the result → RED. NOTE as with the sibling + // above, the assertion is one-sided: switching TotalMilliseconds to TotalSeconds (a 1000x SHRINK) was observed + // to stay GREEN. Both fixtures want a two-sided bound on the absolute difference. public void UnixTime_Convertions_Successfully() { Assert.GreaterOrEqual(ErrorValue, _timeService.UnixTimeFromDateTimeUtc(_timeService.DateTimeUtcNow) - _timeService.UnixTimeNow); @@ -40,6 +54,9 @@ public void UnixTime_Convertions_Successfully() } [Test] + // ADMIT: TimeService.DateTimeUtcNow folds the accumulated _extraTime into the reported wall clock. + // RCR: TimeService.cs DateTimeUtcNow — drop the `_extraTime` term → RED (DateTimeUtcNow no longer reaches dateTime + // + 50.5s). 2026-08-02 public void AddTime_AllTimeTypes_Successfully() { var extraTime = 50.5f; @@ -56,6 +73,10 @@ public void AddTime_AllTimeTypes_Successfully() } [Test] + // ADMIT: TimeService.AddTime accumulates exactly the requested offset, so UnityTimeNow lands within tolerance of + // initial + delta. + // RCR: TimeService.cs AddTime — double the accumulated offset → RED (the Within(0.01) tolerance assertion fails + // while the coarser Less assertion still passes). 2026-08-02 public void AddTime_NegativeValue_SubtractsTime() { var initialUnityTime = _timeService.UnityTimeNow; @@ -68,6 +89,9 @@ public void AddTime_NegativeValue_SubtractsTime() } [Test] + // ADMIT: TimeService.SetInitialTime rebases the clock onto the supplied DateTime. + // RCR: TimeService.cs SetInitialTime — drop the `_initialTime` assignment → RED (DateTimeUtcNow stays on the + // constructor's DateTime.Now, years away from 2025-01-01). 2026-08-02 public void SetInitialTime_ResetsTimeBase() { // SetInitialTime acts as a "reset" by synchronizing the time base diff --git a/Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs b/Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs index fca0d58..093f3cd 100644 --- a/Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs +++ b/Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs @@ -26,6 +26,10 @@ public void ResetStaticState() } [Test] + // ADMIT: VersionServices.EnsureLoaded lazy-loads the version resource on first property access when Bootstrap has + // not yet run. + // RCR: VersionServices.cs EnsureLoaded — drop the LoadVersionData() fallback → RED (_loaded stays false after + // reading all four accessors). 2026-08-02 public void AccessBeforeLoad_AutoLoads() { Assert.IsFalse((bool)LoadedField.GetValue(null), "Precondition: SetUp resets _loaded to false"); @@ -39,6 +43,9 @@ public void AccessBeforeLoad_AutoLoads() } [Test] + // ADMIT: VersionServices.ApplyTextAsset flips _loaded once the version-data TextAsset has parsed. + // RCR: VersionServices.cs ApplyTextAsset — leave _loaded false on the success path → RED (the flag never flips). + // Broad: also reddens the auto-load and post-load accessor tests. 2026-08-02 public void LoadVersionData_Successfully_FlipsLoadedFlag() { VersionServices.LoadVersionData(); @@ -53,6 +60,9 @@ public void LoadVersionData_DoesNotThrow() } [Test] + // ADMIT: VersionServices.VersionInternal formats the loaded VersionData rather than returning a bare fallback. + // RCR: VersionServices.cs VersionInternal — return string.Empty on the loaded branch → RED (Assert.IsNotEmpty + // fails). 2026-08-02 public void AfterLoad_VersionInternal_ContainsExpectedParts() { VersionServices.LoadVersionData(); @@ -65,6 +75,9 @@ public void AfterLoad_VersionInternal_ContainsExpectedParts() } [Test] + // ADMIT: VersionServices.Branch surfaces the loaded VersionData.BranchName instead of the not-loaded fallback. + // RCR: VersionServices.cs Branch — always return string.Empty → RED (Assert.IsNotEmpty fails). A6: assumes the + // host project's version-data.txt carries a non-empty branch. 2026-08-02 public void AfterLoad_Branch_ReturnsNonEmptyString() { VersionServices.LoadVersionData(); @@ -76,6 +89,9 @@ public void AfterLoad_Branch_ReturnsNonEmptyString() } [Test] + // ADMIT: VersionServices.Commit surfaces the loaded VersionData.CommitHash instead of the not-loaded fallback. + // RCR: VersionServices.cs Commit — always return string.Empty → RED (Assert.IsNotEmpty fails). A6: assumes the + // host project's version-data.txt carries a non-empty commit. 2026-08-02 public void AfterLoad_Commit_ReturnsNonEmptyString() { VersionServices.LoadVersionData(); @@ -87,6 +103,10 @@ public void AfterLoad_Commit_ReturnsNonEmptyString() } [Test] + // ADMIT: VersionServices.BuildNumber surfaces the loaded VersionData.BuildNumber instead of the not-loaded + // fallback. + // RCR: VersionServices.cs BuildNumber — always return string.Empty → RED (Assert.IsNotEmpty fails). A6: assumes + // the host project's version-data.txt carries a non-empty build number. 2026-08-02 public void AfterLoad_BuildNumber_ReturnsNonEmptyString() { VersionServices.LoadVersionData(); diff --git a/Tests/EditMode/Unit/VersionServicesTest.cs b/Tests/EditMode/Unit/VersionServicesTest.cs index 20ea6f9..01897a4 100644 --- a/Tests/EditMode/Unit/VersionServicesTest.cs +++ b/Tests/EditMode/Unit/VersionServicesTest.cs @@ -9,6 +9,9 @@ namespace GameLoversEditor.Services.Tests public class VersionServicesTest { [Test] + // ADMIT: VersionServices.FormatInternalVersion appends the build type suffix when one is present. + // RCR: VersionServices.cs FormatInternalVersion — drop the build-type append → RED (the result no longer contains + // "debug"). 2026-08-02 public void FormatInternalVersion_WithBuildType_IncludesBuildType() { var data = new VersionServices.VersionData @@ -27,6 +30,10 @@ public void FormatInternalVersion_WithBuildType_IncludesBuildType() } [Test] + // ADMIT: VersionServices.FormatInternalVersion omits the suffix entirely when BuildType is empty, so the string + // never ends in a bare dot. + // RCR: VersionServices.cs FormatInternalVersion — drop the emptiness guard → RED (the result ends with "."). + // 2026-08-02 public void FormatInternalVersion_WithoutBuildType_OmitsBuildType() { var data = new VersionServices.VersionData diff --git a/Tests/EditMode/Unit/VersioningEditorSettingsTest.cs b/Tests/EditMode/Unit/VersioningEditorSettingsTest.cs index 8d28307..ffa0337 100644 --- a/Tests/EditMode/Unit/VersioningEditorSettingsTest.cs +++ b/Tests/EditMode/Unit/VersioningEditorSettingsTest.cs @@ -9,6 +9,9 @@ namespace GameLoversEditor.Services.Tests public class VersioningEditorSettingsTest { [Test] + // ADMIT: VersioningEditorSettings.IsValidResourcesPath rejects an empty or whitespace path before normalising it. + // RCR: VersioningEditorSettings.cs IsValidResourcesPath — return true from the empty branch → RED (Assert.IsFalse + // fails). 2026-08-02 public void IsValidResourcesPath_EmptyString_ReturnsFalse() { Assert.IsFalse(VersioningEditorSettings.IsValidResourcesPath("", out var error)); @@ -16,6 +19,10 @@ public void IsValidResourcesPath_EmptyString_ReturnsFalse() } [Test] + // ADMIT: VersioningEditorSettings.IsValidResourcesPath requires an Assets/ prefix so the folder is inside the + // AssetDatabase. + // RCR: VersioningEditorSettings.cs IsValidResourcesPath — return true from the prefix branch → RED + // ("Configs/Resources" is accepted). 2026-08-02 public void IsValidResourcesPath_NoAssetsPrefix_ReturnsFalse() { Assert.IsFalse(VersioningEditorSettings.IsValidResourcesPath("Configs/Resources", out var error)); @@ -23,6 +30,9 @@ public void IsValidResourcesPath_NoAssetsPrefix_ReturnsFalse() } [Test] + // ADMIT: VersioningEditorSettings.IsValidResourcesPath rejects `..` segments that would escape the project tree. + // RCR: VersioningEditorSettings.cs IsValidResourcesPath — return true from the dot-dot branch → RED + // ("Assets/../Resources" is accepted). 2026-08-02 public void IsValidResourcesPath_DotDotSegment_ReturnsFalse() { Assert.IsFalse(VersioningEditorSettings.IsValidResourcesPath("Assets/../Resources", out var error)); @@ -30,6 +40,10 @@ public void IsValidResourcesPath_DotDotSegment_ReturnsFalse() } [Test] + // ADMIT: VersioningEditorSettings.IsValidResourcesPath requires a segment named exactly "Resources" so + // Resources.Load can find version-data at runtime. + // RCR: VersioningEditorSettings.cs IsValidResourcesPath — make the containsResources guard unsatisfiable → RED + // ("Assets/Configs/Data" is accepted). 2026-08-02 public void IsValidResourcesPath_NoResourcesSegment_ReturnsFalse() { Assert.IsFalse(VersioningEditorSettings.IsValidResourcesPath("Assets/Configs/Data", out var error)); @@ -37,6 +51,9 @@ public void IsValidResourcesPath_NoResourcesSegment_ReturnsFalse() } [Test] + // ADMIT: VersioningEditorSettings.IsValidResourcesPath accepts the shipped DefaultFolderPath. + // RCR: VersioningEditorSettings.cs IsValidResourcesPath — return false from the success path → RED (the default + // path is reported invalid). 2026-08-02 public void IsValidResourcesPath_ValidDefaultPath_ReturnsTrue() { Assert.IsTrue(VersioningEditorSettings.IsValidResourcesPath( From 8eef6ffece8c0da0b8ab14594cb494faef4c6e9a Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Mon, 3 Aug 2026 13:18:07 +0100 Subject: [PATCH 13/32] test: RCR-backfill 58 services PlayMode tests 59 mutations; 58 observed RED and reverted, production files byte-identical afterwards. EditMode 826/826, PlayMode 299/299. The single holdback confirms a prediction from the drafting pass rather than contradicting it: Despawn_WithCondition_FirstOnly_Successfully stays green when `if (onlyFirst)` is inverted. The EditMode fixture spawns one entity, and the PlayMode typed sibling's predicate matches only one, so ObjectPoolBase.Despawn's onlyFirst break is uncovered REPO-WIDE. That is now observed, not inferred, and is owed a section 13 OPEN row. Co-Authored-By: Claude Opus 5 --- .../Integration/ServiceLifecycleTest.cs | 10 +++ Tests/PlayMode/Unit/CoroutineServiceTest.cs | 62 +++++++++++++++++ Tests/PlayMode/Unit/GameObjectPoolTest.cs | 20 ++++++ .../PlayMode/Unit/GameObjectPoolTypedTest.cs | 45 ++++++++++++ Tests/PlayMode/Unit/TickServiceTest.cs | 68 +++++++++++++++++++ 5 files changed, 205 insertions(+) diff --git a/Tests/PlayMode/Integration/ServiceLifecycleTest.cs b/Tests/PlayMode/Integration/ServiceLifecycleTest.cs index c525601..7a30625 100644 --- a/Tests/PlayMode/Integration/ServiceLifecycleTest.cs +++ b/Tests/PlayMode/Integration/ServiceLifecycleTest.cs @@ -20,6 +20,10 @@ public void Cleanup() } [UnityTest] + // ADMIT: MessageBrokerService.Publish invokes each stored delegate, which is what turns a TickService callback + // into a delivered message. + // RCR: MessageBrokerService.cs Publish — drop the `action(message)` invocation → RED (messageReceived stays false + // after two frames). 2026-08-02 public IEnumerator TickService_WithMessageBroker_PublishesOnTick() { var tickService = new TickService(); @@ -38,6 +42,9 @@ public IEnumerator TickService_WithMessageBroker_PublishesOnTick() } [UnityTest] + // ADMIT: PoolService.Spawn resolves the registered pool and returns its spawned instance. + // RCR: PoolService.cs Spawn — return null instead of delegating to the pool → RED (Assert.IsNotNull(instance) + // fails). 2026-08-02 public IEnumerator PoolService_WithGameObjectPool_FullLifecycle() { var poolService = new PoolService(); @@ -59,6 +66,9 @@ public IEnumerator PoolService_WithGameObjectPool_FullLifecycle() } [Test] + // ADMIT: MainInstaller.Resolve delegates to the private Installer so bound services come back out. + // RCR: MainInstaller.cs Resolve — return default(T) instead of delegating → RED (all three IsNotNull assertions + // fail). Also reddens the PlayMode smoke bind/resolve test. 2026-08-02 public void MainInstaller_BindServices_ResolveAll_Successfully() { MainInstaller.Bind(new TickService()); diff --git a/Tests/PlayMode/Unit/CoroutineServiceTest.cs b/Tests/PlayMode/Unit/CoroutineServiceTest.cs index d59ef31..8b34d41 100644 --- a/Tests/PlayMode/Unit/CoroutineServiceTest.cs +++ b/Tests/PlayMode/Unit/CoroutineServiceTest.cs @@ -34,6 +34,10 @@ public void Dispose() } [UnityTest] + // ADMIT: CoroutineService.StartCoroutine hands the routine to the host MonoBehaviour and returns its Coroutine + // handle. + // RCR: CoroutineService.cs StartCoroutine — return null without starting the routine → RED (_testValue stays 0). + // 2026-08-02 public IEnumerator StartCoroutine_Successfully() { const int testValue1 = 5; @@ -44,6 +48,9 @@ public IEnumerator StartCoroutine_Successfully() } [UnityTest] + // ADMIT: CoroutineService.InternalCoroutine signals the AsyncCoroutine wrapper after the wrapped routine finishes. + // RCR: CoroutineService.cs InternalCoroutine — drop the `completed.Completed()` call → RED (IsCompleted false, + // OnComplete never fires). Also reddens the other natural-completion tests. 2026-08-02 public IEnumerator StartAsyncCoroutine_Successfully() { const int testValue1 = 5; @@ -61,6 +68,9 @@ public IEnumerator StartAsyncCoroutine_Successfully() } [UnityTest] + // ADMIT: CoroutineService.StartAsyncCoroutine seeds the AsyncCoroutine payload with the caller's data. + // RCR: CoroutineService.cs StartAsyncCoroutine — construct the wrapper with `default` instead of `data` → RED + // (the Action callback receives 0, not 10). 2026-08-02 public IEnumerator StartAsyncCoroutine_WithData_Successfully() { const int testValue1 = 5; @@ -78,6 +88,10 @@ public IEnumerator StartAsyncCoroutine_WithData_Successfully() } [UnityTest] + // ADMIT: CoroutineServiceMonoBehaviour.ExternalStopCoroutine is the only path that reaches Unity's + // MonoBehaviour.StopCoroutine for a service-owned handle. + // RCR: CoroutineService.cs ExternalStopCoroutine — make the body a no-op → RED (the routine completes and + // _testValue becomes 5). Also reddens StopAsyncCoroutine_Successfully. 2026-08-02 public IEnumerator StopCoroutine_Successfully() { const int testValue1 = 5; @@ -93,6 +107,10 @@ public IEnumerator StopCoroutine_Successfully() } [UnityTest] + // ADMIT: CoroutineService.StopCoroutine forwards a live handle to the host after its guard passes, and stopping + // the raw handle leaves the async wrapper un-completed. + // RCR: CoroutineService.cs StopCoroutine — drop the forward to ExternalStopCoroutine → RED (routine finishes: + // IsCompleted true and testCompleted 10). Overlaps StopCoroutine_Successfully. 2026-08-02 public IEnumerator StopAsyncCoroutine_Successfully() { const int testValue1 = 5; @@ -116,6 +134,10 @@ public IEnumerator StopAsyncCoroutine_Successfully() } [UnityTest] + // ADMIT: CoroutineService.StopAllCoroutines only bails out when the host is gone; with a live host it must reach + // the host's StopAllCoroutines. + // RCR: CoroutineService.cs StopAllCoroutines — invert the guard to `if (_serviceObject != null)` so it early- + // returns while alive → RED (both routines run to completion). 2026-08-02 public IEnumerator StopAllCoroutines_Successfully() { const int testValue1 = 5; @@ -173,6 +195,9 @@ public IEnumerator StopCoroutine_AfterServiceObjectDestroyed_DoesNotThrowMissing } [UnityTest] + // ADMIT: CoroutineService.Dispose destroys the DontDestroyOnLoad host GameObject it created in the constructor. + // RCR: CoroutineService.cs Dispose — drop the `Object.Destroy(_serviceObject.gameObject)` call → RED (host count + // stays at initialCount + 1). 2026-08-02 public IEnumerator Dispose_DestroysHostGameObject() { var initialCount = Object.FindObjectsByType().Length; @@ -191,6 +216,9 @@ public IEnumerator Dispose_DestroysHostGameObject() } [UnityTest] + // ADMIT: CoroutineService.Dispose guards on the already-nulled _serviceObject so a second Dispose is a no-op. + // RCR: CoroutineService.cs Dispose — delete the `if(_serviceObject == null) return;` guard → RED (the second + // Dispose throws NullReferenceException and DoesNotThrow fails). 2026-08-02 public IEnumerator Dispose_CalledTwice_DoesNotThrow() { var service = new CoroutineService(); @@ -201,6 +229,10 @@ public IEnumerator Dispose_CalledTwice_DoesNotThrow() } [UnityTest] + // ADMIT: CoroutineService.StartDelayCall registers the caller's Action as the wrapper's completion callback before + // starting the delay routine. + // RCR: CoroutineService.cs StartDelayCall — register an empty lambda instead of `call` → RED (`called` stays false + // after the delay elapses). 2026-08-02 public IEnumerator StartDelayCall_Successfully() { bool called = false; @@ -214,6 +246,10 @@ public IEnumerator StartDelayCall_Successfully() } [UnityTest] + // ADMIT: CoroutineService.StartDelayCall seeds the AsyncCoroutine payload with the caller's data so the + // delayed Action receives it. + // RCR: CoroutineService.cs StartDelayCall — construct the wrapper with `default` instead of `data` → RED + // (received stays 0, not 99). 2026-08-02 public IEnumerator StartDelayCall_WithData_Successfully() { int received = 0; @@ -229,6 +265,9 @@ public IEnumerator StartDelayCall_WithData_Successfully() // Stopping via IAsyncCoroutine.StopCoroutine MUST flip IsCompleted/IsRunning so // editor introspection (Services Explorer Coroutine tab) can drop stopped entries. [UnityTest] + // ADMIT: AsyncCoroutine.StopCoroutine flips IsCompleted so editor introspection can drop stopped entries. + // RCR: CoroutineService.cs AsyncCoroutine.StopCoroutine — assign `IsCompleted = false` after stopping → RED + // (IsCompleted stays false). Also reddens AsyncCoroutineStop_CalledTwice_NoOps. 2026-08-02 public IEnumerator AsyncCoroutineStop_FlipsCompletedAndRunning() { IAsyncCoroutine asyncCoroutine = _coroutineService.StartAsyncCoroutine(TestCoroutine(5)); @@ -246,6 +285,10 @@ public IEnumerator AsyncCoroutineStop_FlipsCompletedAndRunning() // triggerOnComplete=true MUST invoke the user OnComplete callback. [UnityTest] + // ADMIT: AsyncCoroutine.StopCoroutine(true) invokes the registered OnComplete callback via OnCompleteTrigger. + // RCR: CoroutineService.cs AsyncCoroutine.StopCoroutine — drop the OnCompleteTrigger() call from the + // triggerOnComplete branch → RED (testCompleted stays 0). Also reddens AsyncCoroutineStop_CalledTwice_NoOps. + // 2026-08-02 public IEnumerator AsyncCoroutineStop_TriggerOnCompleteTrue_InvokesUserCallback() { int testCompleted = 0; @@ -261,6 +304,9 @@ public IEnumerator AsyncCoroutineStop_TriggerOnCompleteTrue_InvokesUserCallback( // triggerOnComplete=false MUST suppress the user OnComplete callback. [UnityTest] + // ADMIT: AsyncCoroutine.StopCoroutine honours triggerOnComplete:false by suppressing the user OnComplete callback. + // RCR: CoroutineService.cs AsyncCoroutine.StopCoroutine — replace the `if (triggerOnComplete)` guard with `if + // (true)` → RED (testCompleted becomes 42). 2026-08-02 public IEnumerator AsyncCoroutineStop_TriggerOnCompleteFalse_SuppressesUserCallback() { int testCompleted = 0; @@ -278,6 +324,10 @@ public IEnumerator AsyncCoroutineStop_TriggerOnCompleteFalse_SuppressesUserCallb // Regression guard for v2.0.0 bug where editor tracking lambda assigned via // OnComplete(...) overwrote (or was overwritten by) user callbacks. [UnityTest] + // ADMIT: AsyncCoroutine.OnCompleteTrigger invokes the user Action registered through OnComplete(Action), which + // editor tracking must never overwrite. + // RCR: CoroutineService.cs AsyncCoroutine.OnCompleteTrigger — drop the `_onComplete?.Invoke()` call → RED + // (userCallbackFired stays false). Broad: also reddens every other Action-callback test. 2026-08-02 public IEnumerator AsyncCoroutineOnComplete_RegisteredAfterCreation_FiresOnNaturalCompletion() { bool userCallbackFired = false; @@ -294,6 +344,10 @@ public IEnumerator AsyncCoroutineOnComplete_RegisteredAfterCreation_FiresOnNatur // guard prevents the user OnComplete callback from re-firing and prevents IsRunning // state from being clobbered. Pairs with AsyncCoroutineStop_CalledTwice_NoOps below. [UnityTest] + // ADMIT: AsyncCoroutine.Completed sets IsCompleted, which is what makes a later StopCoroutine a no-op after + // natural completion. + // RCR: CoroutineService.cs AsyncCoroutine.Completed — assign `IsCompleted = false` → RED (IsCompleted assertion + // fails and the later stop re-fires the callback). Also reddens the natural-completion siblings. 2026-08-02 public IEnumerator AsyncCoroutineStop_AfterNaturalCompletion_NoOps() { int callbackInvocations = 0; @@ -316,6 +370,10 @@ public IEnumerator AsyncCoroutineStop_AfterNaturalCompletion_NoOps() // is a no-op so the user OnComplete callback fires exactly once total. Without the // IsCompleted guard, double-stop would fire OnComplete twice and confuse listeners. [UnityTest] + // ADMIT: AsyncCoroutine.StopCoroutine early-returns when already completed, so a double stop fires the user + // callback exactly once. + // RCR: CoroutineService.cs AsyncCoroutine.StopCoroutine — delete the `if (IsCompleted) return;` guard → RED + // (callbackInvocations is 2). Also reddens AsyncCoroutineStop_AfterNaturalCompletion_NoOps. 2026-08-02 public IEnumerator AsyncCoroutineStop_CalledTwice_NoOps() { int callbackInvocations = 0; @@ -336,6 +394,10 @@ public IEnumerator AsyncCoroutineStop_CalledTwice_NoOps() } [UnityTest] + // ADMIT: AsyncCoroutine.OnCompleteTrigger reads the live Data property at completion time, not a construction- + // time snapshot. + // RCR: CoroutineService.cs AsyncCoroutine.OnCompleteTrigger — invoke with `default` instead of `Data` → RED + // (observed stays 0). Also reddens the two other Action payload tests. 2026-08-02 public IEnumerator AsyncCoroutineDataSetter_AfterStart_UpdatesPayload() { const int initialValue = 5; diff --git a/Tests/PlayMode/Unit/GameObjectPoolTest.cs b/Tests/PlayMode/Unit/GameObjectPoolTest.cs index 35de3a8..eb1f4e7 100644 --- a/Tests/PlayMode/Unit/GameObjectPoolTest.cs +++ b/Tests/PlayMode/Unit/GameObjectPoolTest.cs @@ -50,6 +50,9 @@ public void Cleanup() } [UnityTest] + // ADMIT: GameObjectPool.SpawnEntity re-activates the pooled GameObject that Instantiator deactivated on creation. + // RCR: GameObjectPool.cs GameObjectPool.SpawnEntity — drop the `entity.SetActive(true)` call → RED + // (instance.activeSelf is false). 2026-08-02 public IEnumerator Spawn_InstantiatesPrefab() { var instance = _pool.Spawn(); @@ -62,6 +65,9 @@ public IEnumerator Spawn_InstantiatesPrefab() } [UnityTest] + // ADMIT: GameObjectPool.PostDespawnEntity deactivates the GameObject as it returns to the pool. + // RCR: GameObjectPool.cs GameObjectPool.PostDespawnEntity — drop the `entity.SetActive(false)` call → RED + // (instance.activeSelf is still true after Despawn). 2026-08-02 public IEnumerator Despawn_DeactivatesGameObject() { var instance = _pool.Spawn(); @@ -73,6 +79,10 @@ public IEnumerator Despawn_DeactivatesGameObject() } [UnityTest] + // ADMIT: GameObjectPool.CallOnSpawned resolves IPoolEntitySpawn through GetComponent and invokes OnSpawn on the + // pooled instance. + // RCR: GameObjectPool.cs GameObjectPool.CallOnSpawned — drop the `poolEntity?.OnSpawn()` call → RED + // (mock.SpawnCount stays 0). 2026-08-02 public IEnumerator Spawn_InvokesIPoolEntitySpawn() { var instance = _pool.Spawn(); @@ -87,6 +97,9 @@ public IEnumerator Spawn_InvokesIPoolEntitySpawn() } [UnityTest] + // ADMIT: GameObjectPool.Dispose destroys every GameObject returned by Clear(), spawned ones included. + // RCR: GameObjectPool.cs GameObjectPool.Dispose — drop the `Object.Destroy(obj)` call → RED (the spawned instance + // is still alive next frame). 2026-08-02 public IEnumerator Dispose_DestroysAllInstances() { var instance = _pool.Spawn(); @@ -99,6 +112,9 @@ public IEnumerator Dispose_DestroysAllInstances() } [UnityTest] + // ADMIT: GameObjectPool.Dispose(true) destroys the sample entity the pool was seeded with. + // RCR: GameObjectPool.cs GameObjectPool.Dispose(bool) — drop the `Object.Destroy(SampleEntity)` call → RED + // (_sample survives Dispose(true)). 2026-08-02 public IEnumerator Dispose_WithSampleDestroy_DestroysSample() { _pool.Dispose(true); @@ -109,6 +125,10 @@ public IEnumerator Dispose_WithSampleDestroy_DestroysSample() } [UnityTest] + // ADMIT: GameObjectPool.CallOnSpawned resolves IPoolEntitySpawn through GetComponent and forwards + // the spawn payload. + // RCR: GameObjectPool.cs GameObjectPool.CallOnSpawned — drop the `poolEntity?.OnSpawn(data)` call → RED + // (mock.LastSpawnData stays 0, not 42). 2026-08-02 public IEnumerator SpawnWithData_InvokesIPoolEntitySpawn() { var sampleWithData = new GameObject("SampleWithData"); diff --git a/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs b/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs index 108c154..d833a2c 100644 --- a/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs +++ b/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs @@ -57,6 +57,10 @@ public IEnumerator Dispose_WithDisposeSampleEntityFalse_DoesNotDestroySampleEnti } [UnityTest] + // ADMIT: GameObjectPool.SpawnEntity re-activates the pooled Behaviour's GameObject after the fake-null retry + // loop. + // RCR: GameObjectPool.cs GameObjectPool.SpawnEntity — drop the `entity.gameObject.SetActive(true)` call → RED + // (instance.gameObject.activeSelf is false). 2026-08-02 public IEnumerator Spawn_ReturnsComponentReference() { var instance = _pool.Spawn(); @@ -70,6 +74,9 @@ public IEnumerator Spawn_ReturnsComponentReference() } [UnityTest] + // ADMIT: GameObjectPool.PostDespawnEntity deactivates the Behaviour's GameObject as it returns to the pool. + // RCR: GameObjectPool.cs GameObjectPool.PostDespawnEntity — drop the `entity.gameObject.SetActive(false)` call + // → RED (activeSelf is still true). Also reddens DespawnAll and Despawn_WithCondition_FirstOnly. 2026-08-02 public IEnumerator Despawn_DeactivatesGameObject() { var instance = _pool.Spawn(); @@ -81,6 +88,9 @@ public IEnumerator Despawn_DeactivatesGameObject() } [UnityTest] + // ADMIT: GameObjectPool.CallOnDespawned resolves IPoolEntityDespawn through GetComponent and invokes OnDespawn. + // RCR: GameObjectPool.cs GameObjectPool.CallOnDespawned — drop the `poolEntity?.OnDespawn()` call → RED + // (instance.DespawnCount stays 0). 2026-08-02 public IEnumerator LifecycleHooks_InvokedOnSpawnAndDespawn() { var instance = _pool.Spawn(); @@ -96,6 +106,10 @@ public IEnumerator LifecycleHooks_InvokedOnSpawnAndDespawn() } [UnityTest] + // ADMIT: GameObjectPool.CallOnSpawned resolves IPoolEntitySpawn through GetComponent and forwards + // the spawn payload. + // RCR: GameObjectPool.cs GameObjectPool.CallOnSpawned — drop the `poolEntity?.OnSpawn(data)` call → RED + // (instance.LastSpawnData stays 0, not 42). 2026-08-02 public IEnumerator SpawnWithData_InvokesTypedSpawnHook() { var instance = _pool.Spawn(42); @@ -107,6 +121,9 @@ public IEnumerator SpawnWithData_InvokesTypedSpawnHook() } [UnityTest] + // ADMIT: GameObjectPool.Dispose destroys the GameObject behind every Behaviour returned by Clear(). + // RCR: GameObjectPool.cs GameObjectPool.Dispose — drop the `Object.Destroy(obj.gameObject)` call → RED (both + // spawned instances survive). 2026-08-02 public IEnumerator Dispose_DestroysAllSpawnedInstances() { var instance1 = _pool.Spawn(); @@ -121,6 +138,10 @@ public IEnumerator Dispose_DestroysAllSpawnedInstances() } [UnityTest] + // ADMIT: ObjectPoolBase.DespawnAll walks the spawned list down to index 0, so the first-spawned entity is + // despawned too. + // RCR: ObjectPool.cs DespawnAll — change the loop bound to `i > 0` → RED (instance1 stays active and + // SpawnedReadOnly.Count is 1). 2026-08-02 public IEnumerator DespawnAll_DeactivatesAllSpawnedInstances() { var instance1 = _pool.Spawn(); @@ -144,6 +165,9 @@ public IEnumerator SampleEntity_ReturnsSampleReference() } [UnityTest] + // ADMIT: ObjectPoolBase.SpawnedReadOnly exposes the live SpawnedEntities backing list, not a detached copy. + // RCR: ObjectPool.cs SpawnedReadOnly — return a fresh empty list instead → RED (count stays 0 after Spawn). Also + // reddens the other SpawnedReadOnly-count assertions in this fixture. 2026-08-02 public IEnumerator SpawnedReadOnly_ReflectsSpawnedEntities() { Assert.AreEqual(0, _pool.SpawnedReadOnly.Count); @@ -157,6 +181,9 @@ public IEnumerator SpawnedReadOnly_ReflectsSpawnedEntities() } [UnityTest] + // ADMIT: ObjectPoolBase.IsSpawned returns true for the first spawned entity that satisfies the predicate. + // RCR: ObjectPool.cs IsSpawned — invert the predicate test → RED (matching returns false, non-matching returns + // true). 2026-08-02 public IEnumerator IsSpawned_ReturnsTrueWhenMatch() { var instance = _pool.Spawn(); @@ -182,6 +209,10 @@ public IEnumerator Despawn_WithCondition_FirstOnly_Successfully() } [UnityTest] + // ADMIT: ObjectPoolBase.Despawn(bool, Func) steps the index back after a successful removal so adjacent matches + // are not skipped. + // RCR: ObjectPool.cs Despawn(bool, Func) — delete the `i--` step-back → RED (only the first of the two distinct + // instances is despawned, count 1). 2026-08-02 public IEnumerator Despawn_WithCondition_AllMatching_DespawnsAll() { _pool.Spawn(); @@ -194,6 +225,9 @@ public IEnumerator Despawn_WithCondition_AllMatching_DespawnsAll() } [UnityTest] + // ADMIT: ObjectPoolBase.Reset re-seeds _sampleEntity with the new sample before re-filling the stack. + // RCR: ObjectPool.cs Reset — drop the `_sampleEntity = sampleEntity` assignment → RED (SampleEntity still points + // at the old sample). 2026-08-02 public IEnumerator Reset_ClearsAndReinitializesPool() { _pool.Spawn(); @@ -212,6 +246,10 @@ public IEnumerator Reset_ClearsAndReinitializesPool() } [UnityTest] + // ADMIT: GameObjectPool.PostDespawnEntity reparents a despawned instance under the sample entity's parent when + // DespawnToSampleParent is set. + // RCR: GameObjectPool.cs GameObjectPool.PostDespawnEntity — drop the SetParent call → RED + // (instance.transform.parent stays null, not the sample's parent). 2026-08-02 public IEnumerator DespawnToSampleParent_ReparentsOnDespawn() { var parent = new GameObject("Parent"); @@ -235,6 +273,10 @@ public IEnumerator DespawnToSampleParent_ReparentsOnDespawn() } [UnityTest] + // ADMIT: GameObjectPool.Dispose skips Unity fake-null entries because `.gameObject` on a destroyed Behaviour + // throws MissingReferenceException. + // RCR: GameObjectPool.cs GameObjectPool.Dispose — delete the `if (obj == null) continue;` guard → RED + // (MissingReferenceException, DoesNotThrow fails). 2026-08-02 public IEnumerator Dispose_AfterDespawnedInstanceDestroyedExternally_DoesNotThrow() { var externalParent = new GameObject("ExternalParent"); @@ -252,6 +294,9 @@ public IEnumerator Dispose_AfterDespawnedInstanceDestroyedExternally_DoesNotThro } [UnityTest] + // ADMIT: GameObjectPool.Dispose(true) destroys the GameObject behind the sample Behaviour. + // RCR: GameObjectPool.cs GameObjectPool.Dispose(bool) — drop the `Object.Destroy(SampleEntity.gameObject)` call + // → RED (_sampleGo survives Dispose(true)). 2026-08-02 public IEnumerator DisposeWithSampleDestroy_DestroysSampleGameObject() { _pool.Dispose(disposeSampleEntity: true); diff --git a/Tests/PlayMode/Unit/TickServiceTest.cs b/Tests/PlayMode/Unit/TickServiceTest.cs index fa22fe3..f3447a8 100644 --- a/Tests/PlayMode/Unit/TickServiceTest.cs +++ b/Tests/PlayMode/Unit/TickServiceTest.cs @@ -26,6 +26,10 @@ public void Dispose() } [UnityTest] + // ADMIT: TickService.Update forwards the elapsed time as `time - LastTickTime`, so a subscriber must never see a + // negative deltaTime. + // RCR: TickService.cs Update — flip the subtraction to `LastTickTime - time` → RED (receivedDelta is negative). + // Also reddens the LateUpdate and RealTime siblings, which read the same sign. 2026-08-02 public IEnumerator SubscribeOnUpdate_ReceivesDeltaTime() { float receivedDelta = -1f; @@ -38,6 +42,10 @@ public IEnumerator SubscribeOnUpdate_ReceivesDeltaTime() } [UnityTest] + // ADMIT: TickService.Update rate-limits a buffered subscriber by skipping until `LastTickTime + DeltaTime` has + // elapsed. + // RCR: TickService.cs Update — drop the `+ tickData.DeltaTime` term from the skip guard → RED (callCount is + // already ≥1 at the half-interval assertion). 2026-08-02 public IEnumerator SubscribeOnUpdate_WithDeltaBuffer_InvokesAtInterval() { int callCount = 0; @@ -52,6 +60,10 @@ public IEnumerator SubscribeOnUpdate_WithDeltaBuffer_InvokesAtInterval() } [UnityTest] + // ADMIT: TickService.Update carries the modulo remainder back into LastTickTime so a buffered subscriber does not + // drift a whole interval per tick. + // RCR: TickService.cs Update — replace the overflow with `-tickData.DeltaTime` so LastTickTime jumps forward + // instead of back → RED (only one tick lands inside the 2.5-interval wait). 2026-08-02 public IEnumerator SubscribeOnUpdate_TimeOverflow_CarriesOverflow() { float interval = 0.05f; @@ -89,6 +101,10 @@ public IEnumerator SubscribeOnUpdate_ZeroDeltaTimeWithOverflowToNextTick_TicksEv } [UnityTest] + // ADMIT: TickService.Update reads Time.realtimeSinceStartup for a RealTime subscriber, so ticking survives + // Time.timeScale == 0. + // RCR: TickService.cs Update — hard-code `var time = Time.time;` → RED (deltaTime is ≤0 under timeScale 0 because + // LastTickTime was stamped from realtime). 2026-08-02 public IEnumerator SubscribeOnUpdate_RealTime_UsesUnscaledTime() { float initialTimeScale = Time.timeScale; @@ -105,6 +121,10 @@ public IEnumerator SubscribeOnUpdate_RealTime_UsesUnscaledTime() } [UnityTest] + // ADMIT: TickService.UnsubscribeOnUpdate matches on delegate identity, so a subscriber that unsubscribes itself + // from inside its own callback is not ticked again. + // RCR: TickService.cs UnsubscribeOnUpdate — invert the `Action == action` match to `!=` → RED (callCount is 2, not + // 1). Also reddens Unsubscribe_UmbrellaOverload. 2026-08-02 public IEnumerator UnsubscribeOnUpdate_DuringCallback_SafelyRemoves() { int callCount = 0; @@ -130,6 +150,9 @@ private class TickSubscriber } [UnityTest] + // ADMIT: TickService.UnsubscribeAll() fans out to all three per-list clears, including the Update list. + // RCR: TickService.cs UnsubscribeAll() — change the `UnsubscribeAllOnUpdate()` call to a second + // `UnsubscribeAllOnFixedUpdate()` → RED (both update subscribers still tick). 2026-08-02 public IEnumerator UnsubscribeAll_RemovesAllSubscribers() { var sub1 = new TickSubscriber(); @@ -147,6 +170,10 @@ public IEnumerator UnsubscribeAll_RemovesAllSubscribers() } [UnityTest] + // ADMIT: TickService.UnsubscribeAll(object) fans out to the per-list subscriber-scoped removals, including the + // Update list. + // RCR: TickService.cs UnsubscribeAll(object) — change the `UnsubscribeAllOnUpdate(subscriber)` call to + // `UnsubscribeAllOnFixedUpdate(subscriber)` → RED (sub1 keeps ticking). 2026-08-02 public IEnumerator UnsubscribeAll_BySubscriber_RemovesOnlyThatSubscriber() { var sub1 = new TickSubscriber(); @@ -164,6 +191,9 @@ public IEnumerator UnsubscribeAll_BySubscriber_RemovesOnlyThatSubscriber() } [UnityTest] + // ADMIT: TickService.Dispose destroys the DontDestroyOnLoad host GameObject it created in the constructor. + // RCR: TickService.cs Dispose — drop the `Object.Destroy(_tickObject.gameObject)` call → RED (the host count stays + // at initialCount + 1 after Dispose). 2026-08-02 public IEnumerator Dispose_DestroysGameObject() { var initialCount = Object.FindObjectsByType().Length; @@ -178,6 +208,10 @@ public IEnumerator Dispose_DestroysGameObject() } [UnityTest] + // ADMIT: TickService.OnFixedUpdate forwards Time.fixedTime to every fixed-update subscriber, so the received value + // is never negative. + // RCR: TickService.cs OnFixedUpdate — forward `-1f` instead of `Time.fixedTime` → RED (receivedDelta is negative). + // 2026-08-02 public IEnumerator SubscribeOnFixedUpdate_ReceivesDeltaTime() { float receivedDelta = -1f; @@ -190,6 +224,10 @@ public IEnumerator SubscribeOnFixedUpdate_ReceivesDeltaTime() } [UnityTest] + // ADMIT: TickService's constructor wires the host MonoBehaviour's LateUpdate callback to the late-update list fan- + // out. + // RCR: TickService.cs TickService() — wire OnLateUpdate to OnFixedUpdate instead → RED (receivedDelta stays -1). + // Also reddens the two other late-update tests. 2026-08-02 public IEnumerator SubscribeOnLateUpdate_ReceivesDeltaTime() { float receivedDelta = -1f; @@ -202,6 +240,9 @@ public IEnumerator SubscribeOnLateUpdate_ReceivesDeltaTime() } [UnityTest] + // ADMIT: TickService.UnsubscribeOnFixedUpdate matches on delegate identity before removing the fixed-update entry. + // RCR: TickService.cs UnsubscribeOnFixedUpdate — invert the `Action == action` match to `!=` → RED (callCount + // keeps rising after unsubscribe). Also reddens Unsubscribe_UmbrellaOverload. 2026-08-02 public IEnumerator UnsubscribeOnFixedUpdate_RemovesCallback() { int callCount = 0; @@ -221,6 +262,9 @@ public IEnumerator UnsubscribeOnFixedUpdate_RemovesCallback() } [UnityTest] + // ADMIT: TickService.UnsubscribeOnLateUpdate matches on delegate identity before removing the late-update entry. + // RCR: TickService.cs UnsubscribeOnLateUpdate — invert the `Action == action` match to `!=` → RED (callCount keeps + // rising after unsubscribe). Also reddens Unsubscribe_UmbrellaOverload. 2026-08-02 public IEnumerator UnsubscribeOnLateUpdate_RemovesCallback() { int callCount = 0; @@ -240,6 +284,9 @@ public IEnumerator UnsubscribeOnLateUpdate_RemovesCallback() } [UnityTest] + // ADMIT: TickService.UnsubscribeAllOnUpdate() clears the update list, not one of the sibling lists. + // RCR: TickService.cs UnsubscribeAllOnUpdate() — clear _onFixedUpdateList instead → RED (both subscribers still + // tick). Also reddens UnsubscribeAll_RemovesAllSubscribers, which routes through it. 2026-08-02 public IEnumerator UnsubscribeAllOnUpdate_RemovesAllUpdateSubscribers() { var sub1 = new TickSubscriber(); @@ -257,6 +304,9 @@ public IEnumerator UnsubscribeAllOnUpdate_RemovesAllUpdateSubscribers() } [UnityTest] + // ADMIT: TickService.UnsubscribeAllOnUpdate(object) removes the entries whose Subscriber matches, and only those. + // RCR: TickService.cs UnsubscribeAllOnUpdate(object) — invert the RemoveAll predicate to `!=` → RED (sub1 keeps + // ticking and sub2 is dropped). 2026-08-02 public IEnumerator UnsubscribeAllOnUpdate_BySubscriber_RemovesOnlyThatSubscriber() { var sub1 = new TickSubscriber(); @@ -274,6 +324,9 @@ public IEnumerator UnsubscribeAllOnUpdate_BySubscriber_RemovesOnlyThatSubscriber } [UnityTest] + // ADMIT: TickService.UnsubscribeAllOnFixedUpdate() clears the fixed-update list, not one of the sibling lists. + // RCR: TickService.cs UnsubscribeAllOnFixedUpdate() — clear _onLateUpdateList instead → RED (both fixed-update + // subscribers still tick). 2026-08-02 public IEnumerator UnsubscribeAllOnFixedUpdate_RemovesAllFixedUpdateSubscribers() { var sub1 = new TickSubscriber(); @@ -292,6 +345,10 @@ public IEnumerator UnsubscribeAllOnFixedUpdate_RemovesAllFixedUpdateSubscribers( } [UnityTest] + // ADMIT: TickService.UnsubscribeAllOnFixedUpdate(object) removes the entries whose Subscriber matches, and only + // those. + // RCR: TickService.cs UnsubscribeAllOnFixedUpdate(object) — invert the RemoveAll predicate to `!=` → RED (sub1 + // keeps ticking and sub2 is dropped). 2026-08-02 public IEnumerator UnsubscribeAllOnFixedUpdate_BySubscriber_RemovesOnlyThatSubscriber() { var sub1 = new TickSubscriber(); @@ -310,6 +367,9 @@ public IEnumerator UnsubscribeAllOnFixedUpdate_BySubscriber_RemovesOnlyThatSubsc } [UnityTest] + // ADMIT: TickService.UnsubscribeAllOnLateUpdate() clears the late-update list, not one of the sibling lists. + // RCR: TickService.cs UnsubscribeAllOnLateUpdate() — clear _onFixedUpdateList instead → RED (both late-update + // subscribers still tick). 2026-08-02 public IEnumerator UnsubscribeAllOnLateUpdate_RemovesAllLateUpdateSubscribers() { var sub1 = new TickSubscriber(); @@ -328,6 +388,10 @@ public IEnumerator UnsubscribeAllOnLateUpdate_RemovesAllLateUpdateSubscribers() } [UnityTest] + // ADMIT: TickService.UnsubscribeAllOnLateUpdate(object) removes the entries whose Subscriber matches, and only + // those. + // RCR: TickService.cs UnsubscribeAllOnLateUpdate(object) — invert the RemoveAll predicate to `!=` → RED (sub1 + // keeps ticking and sub2 is dropped). 2026-08-02 public IEnumerator UnsubscribeAllOnLateUpdate_BySubscriber_RemovesOnlyThatSubscriber() { var sub1 = new TickSubscriber(); @@ -346,6 +410,10 @@ public IEnumerator UnsubscribeAllOnLateUpdate_BySubscriber_RemovesOnlyThatSubscr } [UnityTest] + // ADMIT: TickService.Unsubscribe(action) is the umbrella that forwards to all three per-list removals, including + // the fixed-update one. + // RCR: TickService.cs Unsubscribe — replace the `UnsubscribeOnFixedUpdate(action)` forward with a duplicate + // `UnsubscribeOnUpdate(action)` → RED (callCount keeps rising from FixedUpdate). 2026-08-02 public IEnumerator Unsubscribe_UmbrellaOverload_RemovesActionFromAllThreeUpdateLists() { int callCount = 0; From aa577c39bbc8f4629aef98be4642110674abe59c Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Mon, 3 Aug 2026 23:57:15 +0100 Subject: [PATCH 14/32] test: verdict pass - delete 3 A3 tests, keep 2 unproven A5 Deleted (A3 - no production symbol in the causal chain): - CommandServiceTest.ServerCommand_ExecuteLogic_InvokedWithGameLogic calls a private test-local class's ExecuteLogic directly; CommandService never runs and IGameServerCommand is a bare declaration. - RngServiceTest.Next_SameSeed_ReturnsDeterministicSequence and Nextfloat_ReturnsDeterministicSequence compare two runs of the SAME pure function, so any edit to GenerateRngState/NextNumber moves both sides identically and equality is preserved by construction. Determinism against a recorded expected sequence would be falsifiable; self-comparison is not. KEPT: RequestAsset_UnknownId_ThrowsMissingMember and IsValidNamespace_SingleSegment_ReturnsTrue - A5 candidates with narrow sibling evidence but no probe. Narrow evidence alone proved insufficient elsewhere in this pass: UnloadUiSet_WithOpenPresenters_UnloadsAll had radius-2 evidence and its probe still came back RED-OK. EditMode 806/806, PlayMode 295/295. Co-Authored-By: Claude Opus 5 --- Tests/AGENTS.md | 23 ++++++++++++++ Tests/EditMode/Unit/CommandServiceTest.cs | 10 ------- Tests/EditMode/Unit/RngServiceTest.cs | 30 ++----------------- .../Smoke/ServicesBootstrapSmokeTest.cs | 9 ++++++ Tests/PlayMode/Unit/TickServiceTest.cs | 4 +++ 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index 00e5d42..179ea54 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -144,6 +144,29 @@ have different answers: | **A5 duplicate** — the only mutation that reddens it already belongs to a sibling | pins nothing new | **Delete**, naming the surviving sibling in the commit body | | **D2 overclaim** — the name promises behaviour the body cannot detect | name is a lie | **Strengthen the assertion**, or rename to what it actually checks | | **UNFALSIFIABLE** — real behaviour, but double-guarded or otherwise unbreakable one line at a time | valid | **Keep**, with the exemption comment above | +| **SHARED-PATH** — no unique one-line pin, but the test was *observed* reddening under a broader mutation | valid | **Keep**, recording the covering mutation and its blast radius | + +**SHARED-PATH exists because blast radius measures specificity, not value.** A test that +only reddens under a broad mutation still catches that regression — an integration test +that dies when `UiService.CloseUi` is gutted is doing its job, even though no single line +is *its* line. Without this row the table offers only delete-or-strengthen, and such tests +get deleted for the crime of being integration tests. + +```csharp +// ADMIT: exercises 's ; no unique one-line pin. +// RCR: no isolated mutation — reddens under 's mutation (radius N, verified). +// Shared-path coverage, not a duplicate. +``` + +The radius must be a **recorded observation**, not an estimate. This is also the row most +easily abused: "some mutation somewhere reddened it" is not the standard. Distinguish it +from A5 by asking what the covering mutation actually broke — if it broke the one narrow +guard the sibling owns, this is a duplicate; if it broke a path both tests legitimately +traverse, this is shared-path coverage. + +A cluster of tests that all die to the same broad mutation is **over-provisioned, not +individually worthless**. Thinning it is a deliberate editorial decision made by a human +looking at what each assertion adds — never an automatic consequence of the verdict pass. **A3 is checked first, and it is the commonest way the exemption gets abused.** UNFALSIFIABLE is for behaviour this package genuinely owns but cannot be broken one diff --git a/Tests/EditMode/Unit/CommandServiceTest.cs b/Tests/EditMode/Unit/CommandServiceTest.cs index ebdc510..c896135 100644 --- a/Tests/EditMode/Unit/CommandServiceTest.cs +++ b/Tests/EditMode/Unit/CommandServiceTest.cs @@ -61,15 +61,5 @@ public void ExecuteCommand_Successfully() _gameLogicMockup.Received().CallMockup(Arg.Is(payload)); } - [Test] - public void ServerCommand_ExecuteLogic_InvokedWithGameLogic() - { - var payload = 7; - IGameServerCommand command = new ServerCommandMockup { Payload = payload }; - - command.ExecuteLogic(_gameLogicMockup); - - _gameLogicMockup.Received(1).CallMockup(Arg.Is(payload)); - } } } \ No newline at end of file diff --git a/Tests/EditMode/Unit/RngServiceTest.cs b/Tests/EditMode/Unit/RngServiceTest.cs index bae37e9..f01bd3a 100644 --- a/Tests/EditMode/Unit/RngServiceTest.cs +++ b/Tests/EditMode/Unit/RngServiceTest.cs @@ -21,19 +21,6 @@ public void Init() _rngService = new RngService(_rngData); } - [Test] - public void Next_SameSeed_ReturnsDeterministicSequence() - { - var sequence1 = new int[10]; - for (var i = 0; i < 10; i++) sequence1[i] = _rngService.Next; - - var data2 = RngService.CreateRngData(Seed); - var rng2 = new RngService(data2); - var sequence2 = new int[10]; - for (var i = 0; i < 10; i++) sequence2[i] = rng2.Next; - - Assert.AreEqual(sequence1, sequence2); - } [Test] // ADMIT: RngService.PeekRange(int,int,bool) draws from a copy of the state so Peek never advances the live @@ -162,20 +149,6 @@ public void CreateRngData_InitializesCorrectly() Assert.AreEqual(56, data.State.Length); } - [Test] - public void Nextfloat_ReturnsDeterministicSequence() - { - floatP f1 = _rngService.Nextfloat; - floatP f2 = _rngService.Nextfloat; - - var data2 = RngService.CreateRngData(Seed); - var rng2 = new RngService(data2); - floatP f1b = rng2.Nextfloat; - floatP f2b = rng2.Nextfloat; - - Assert.AreEqual(f1, f1b); - Assert.AreEqual(f2, f2b); - } [Test] // ADMIT: RngService.Peekfloat draws through PeekRange, which works on a copy of the state, so repeated reads @@ -195,6 +168,9 @@ public void Peekfloat_DoesNotAdvanceState() } [Test] + // ADMIT: exercises RngService.Range(floatP,floatP,int[],bool)'s scale-and-offset path; no unique one-line pin. + // RCR: no isolated mutation -- reddens under Range_IntStaticOverload_StaysWithinBoundsAndAdvancesState's + // mutation (offset by max instead of min; radius 5, observed). Shared-path coverage, not a duplicate. public void RangeFloat_ReturnsValueInRange() { floatP min = (floatP)0f; diff --git a/Tests/PlayMode/Smoke/ServicesBootstrapSmokeTest.cs b/Tests/PlayMode/Smoke/ServicesBootstrapSmokeTest.cs index 845b41e..13a3cd0 100644 --- a/Tests/PlayMode/Smoke/ServicesBootstrapSmokeTest.cs +++ b/Tests/PlayMode/Smoke/ServicesBootstrapSmokeTest.cs @@ -18,6 +18,9 @@ public void Cleanup() } [Test] + // ADMIT: bootstrap regression -- the services assembly stops loading, or a service ctor gains a hard dependency. + // RCR: none required -- Tests/AGENTS.md §1 exempts Smoke/ fixtures from A1/A2; construction-without-throwing + // is the whole contract here. public void AllServices_Instantiate_WithoutException() { Assert.DoesNotThrow(() => new MessageBrokerService()); @@ -46,6 +49,9 @@ public void CoroutineService_CreatesGameObject() } [Test] + // ADMIT: bootstrap regression -- the MainInstaller bind/resolve round trip breaks at assembly load. + // RCR: none required -- Smoke/ exemption (Tests/AGENTS.md §1). The same round trip is pinned in depth by + // ServiceLifecycleTest.MainInstaller_BindServices_ResolveAll_Successfully; this copy is the bootstrap canary. public void MainInstaller_BindResolve_Works() { var broker = new MessageBrokerService(); @@ -54,6 +60,9 @@ public void MainInstaller_BindResolve_Works() } [Test] + // ADMIT: bootstrap regression -- publishing into an empty broker throws at assembly load. + // RCR: none required -- Smoke/ exemption (Tests/AGENTS.md §1). The no-subscribers early return is duplicated + // verbatim in Publish and PublishSafe, so no anchor is unique to the overload this test calls. public void MessageBroker_PublishWithoutSubscribers_Works() { var broker = new MessageBrokerService(); diff --git a/Tests/PlayMode/Unit/TickServiceTest.cs b/Tests/PlayMode/Unit/TickServiceTest.cs index f3447a8..5f1bce0 100644 --- a/Tests/PlayMode/Unit/TickServiceTest.cs +++ b/Tests/PlayMode/Unit/TickServiceTest.cs @@ -439,6 +439,10 @@ public IEnumerator Unsubscribe_UmbrellaOverload_RemovesActionFromAllThreeUpdateL } [Test] + // ADMIT: TickService does NOT enforce a singleton -- the ctor's `_tickObject != null` guard reads an instance + // field and is therefore always false, so each construction adds its own TickServiceMonoBehaviour. + // RCR: none exists -- the assertion pins the ABSENCE of singleton enforcement; only a structural change + // (making the guard static) flips it. Unfalsifiable one line at a time, not a duplicate. public void MultipleInstances_CreateMultipleGameObjects() { // Note: The service doesn't enforce singleton, but it throws if _tickObject is already set From 422269257e0ed31131774ea7a260600aab9041b7 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 00:02:55 +0100 Subject: [PATCH 15/32] docs: record the verified coverage baseline and the OPEN rows the audit proved First trustworthy coverage figure for this repo. Regenerated with -debugCodeOptimization, all 11 GameLovers assemblies in scope, test and sample assemblies excluded. Repo-wide runtime coverage is 74.1% (6609/8922). Do not compare against any earlier number. 41.8% was stale, wrongly scoped to 6 assemblies, and diluted by Editor code; 38.3% was compiled in Release, which silently shrank the denominator ~40%. The register now names the sanity check that catches a repeat: MathfloatP must report ~1002 coverable lines, not 637. The OPEN rows added here are findings the mutation pass PROVED rather than suspected - each one is a mutation that was applied and observed leaving its test green. Co-Authored-By: Claude Opus 5 --- Tests/AGENTS.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index 179ea54..e3ebbcd 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -282,15 +282,16 @@ Why both keys: `RunSettings.Instance` is a lazy-loaded singleton (`ResourcesLoad ## 13. Coverage Register -**Baseline — runtime assembly: 84.4% (1050/1244), measured 2026-08-02.** +**Baseline — runtime assembly: 84.4% (1050/1244), measured 2026-08-04.** Editor assembly: **4.8% (153/3162)** — near-zero by policy; the ACCEPTED (iii) rows below are why. +Repo-wide runtime coverage is **74.1% (6609/8922)** across all 11 assemblies. + +Regenerate with `Tools/coverage.sh`, which prints the runtime/Editor split. Steer by +the **runtime** figure: Editor code is ~48% of coverable lines and accepted-untestable, +so the combined number (41.1%) can never meaningfully move. Sanity-check any rerun by +confirming `MathfloatP` reports ~1002 coverable lines — a smaller figure means +`-debugCodeOptimization` was missing and the denominator silently shrank ~40%. -Regenerate with `Tools/coverage.sh`, which prints the runtime/Editor split. -Steer by the **runtime** figure: Editor code is ~48% of the repo's coverable -lines and is accepted-untestable, so the combined number (41.0%) can never -meaningfully move. Do not compare against any figure recorded before this date — -earlier reports were produced without `-debugCodeOptimization` (Release mode -shrinks the denominator ~40%) or with test/sample assemblies leaking into scope. Every untested symbol worth naming is either ACCEPTED (justified — do not re-report) or OPEN (a real gap, owed a test). An untested symbol in neither state @@ -322,6 +323,10 @@ The count of OPEN rows is the honest coverage-debt number. | `VersionServices.IsOutdatedVersion` (`Runtime/VersionServices.cs`) | OPEN | Owed: only coverage was a local reimplementation of the algorithm in `VersionServicesTest.cs`; a real test against the actual method is owed. | 2026-07-31 | | `AddressableIdsGeneratorUtils.ResolveSanitizedEnumName` 3-way collision (`Editor/AddressableIds/AddressableIdsGeneratorUtils.cs:531-539`) | OPEN | The two-address collision was fixed 2026-08-01 (`AppendAddressEnumMembers` now emits the disambiguated `name`). A deeper edge case remains: a THIRD address colliding with the same base name AND filetype re-derives the identical `"{name}_{filetype}"` suffix (the fallback only ever adds one suffix level and the collision check is against the original `name`, not against previously-suffixed candidates), so 3+ colliding addresses can still emit duplicates. Owed: either a numeric fallback (`_2`, `_3`, ...) or checking collision against the full history of emitted names, not just base names. | 2026-08-01 | +| `ObjectPoolBase.Despawn` `onlyFirst` break (`Runtime/Pooling/ObjectPool.cs`) | OPEN | Owed: the branch is uncovered **repo-wide**, proven not inferred — inverting `if (onlyFirst)` left `Despawn_WithCondition_FirstOnly_Successfully` GREEN (the EditMode fixture spawns one entity; the PlayMode typed sibling's predicate matches only one). A test must spawn two matching entities and assert exactly one is despawned. | 2026-08-04 | +| `TimeService.UnityTimeFromDateTimeUtc` / `UnixTimeFromDateTimeUtc` shrink direction (`Runtime/TimeService.cs`) | OPEN | Owed: both fixtures assert `GreaterOrEqual(ErrorValue, converted - now)`, which bounds the difference only from ABOVE. Negating `_initialUnityTime`, and swapping `TotalMilliseconds` for `TotalSeconds` (a 1000x shrink), were both observed GREEN. Needs a two-sided bound on the absolute difference. | 2026-08-04 | +| `RngService.Peekfloat` state-advance detection (`Runtime/RngService.cs`) | OPEN | Owed: `Peekfloat` draws over `(0, floatP.MaxValue)`, where consecutive draws saturate to the same `floatP` — so making `PeekRange` consume the LIVE state was observed GREEN. The test is precision-blind to the advance its name claims to catch. | 2026-08-04 | + ## 14. Update Policy Update this file when: - Test conventions change (new asmdef references, assertion style, naming patterns, new test categories) From 07abdf1fa78918f626046fcd4f84463ebdd1931e Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 00:39:46 +0100 Subject: [PATCH 16/32] test: replace one-sided TimeService conversion bounds with two-sided ones Three assertion pairs used Assert.GreaterOrEqual(ErrorValue, converted - now), which bounds the difference only from ABOVE. Any regression that made a conversion SMALLER was invisible: negating _initialUnityTime, and swapping TotalMilliseconds for TotalSeconds - a 1000x shrink in a time conversion - were both observed staying GREEN. Now Assert.LessOrEqual(Math.Abs(diff), tolerance). Re-verified: both shrink mutations redden, isolated. The third pair (DateTime_Convertions_Successfully) had the same defect and was NOT in the audit's findings - it surfaced while fixing the other two. Same fix applied. Adds UnixErrorMillis = 50d. The unix/DateTime comparisons are in MILLISECONDS while ErrorValue = 0.01f is seconds; reusing ErrorValue there would have made the bound absurdly tight and the test flaky. The two scales now have separate tolerances. RCR: UnityTime_Convertions_Successfully <- TimeService.cs UnityTimeFromDateTimeUtc negate the + _initialUnityTime term (RED, isolated) RCR: UnixTime_Convertions_Successfully <- TimeService.cs UnixTimeFromDateTimeUtc return TotalSeconds instead of TotalMilliseconds (RED, isolated) EditMode 806/806. Co-Authored-By: Claude Opus 5 --- Tests/EditMode/Unit/TimeServiceTest.cs | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Tests/EditMode/Unit/TimeServiceTest.cs b/Tests/EditMode/Unit/TimeServiceTest.cs index 9c6e2d2..f0afc00 100644 --- a/Tests/EditMode/Unit/TimeServiceTest.cs +++ b/Tests/EditMode/Unit/TimeServiceTest.cs @@ -10,6 +10,8 @@ namespace GameLoversEditor.Services.Tests public class TimeServiceTest { private const float ErrorValue = 0.01f; + // Unix/DateTime conversions are compared in MILLISECONDS, so they need their own tolerance. + private const double UnixErrorMillis = 50d; private TimeService _timeService; [SetUp] @@ -25,32 +27,30 @@ public void Init() // trip is off by ~1000x). Also reddens UnityTime_Convertions. 2026-08-02 public void DateTime_Convertions_Successfully() { - Assert.GreaterOrEqual(ErrorValue, (_timeService.DateTimeUtcFromUnityTime(_timeService.UnityTimeNow) - _timeService.DateTimeUtcNow).TotalMilliseconds); - Assert.GreaterOrEqual(ErrorValue, (_timeService.DateTimeUtcFromUnixTime(_timeService.UnixTimeNow) - _timeService.DateTimeUtcNow).TotalMilliseconds); + Assert.LessOrEqual(Math.Abs((_timeService.DateTimeUtcFromUnityTime(_timeService.UnityTimeNow) - _timeService.DateTimeUtcNow).TotalMilliseconds), UnixErrorMillis); + Assert.LessOrEqual(Math.Abs((_timeService.DateTimeUtcFromUnixTime(_timeService.UnixTimeNow) - _timeService.DateTimeUtcNow).TotalMilliseconds), UnixErrorMillis); } [Test] // ADMIT: TimeService.UnityTimeFromDateTimeUtc/FromUnixTime rebase onto _initialUnityTime so a converted - // instant lands near UnityTimeNow. - // RCR: TimeService.cs UnityTimeFromDateTimeUtc — add +1000f to the returned offset → RED. NOTE the assertion is - // ONE-SIDED (`GreaterOrEqual(ErrorValue, diff)`), so it only catches conversions that grow: negating - // `_initialUnityTime` instead was observed to stay GREEN. A regression that shrinks the conversion is invisible. + // instant lands within ErrorValue of UnityTimeNow in EITHER direction. + // RCR: TimeService.cs UnityTimeFromDateTimeUtc — negate the `+ _initialUnityTime` term → RED (isolated). + // The bound is two-sided deliberately: this exact shrink passed unnoticed under a one-sided assertion. public void UnityTime_Convertions_Successfully() { - Assert.GreaterOrEqual(ErrorValue, _timeService.UnityTimeFromDateTimeUtc(_timeService.DateTimeUtcNow) - _timeService.UnityTimeNow); - Assert.GreaterOrEqual(ErrorValue, _timeService.UnityTimeFromUnixTime(_timeService.UnixTimeNow) - _timeService.UnityTimeNow); + Assert.LessOrEqual(Math.Abs(_timeService.UnityTimeFromDateTimeUtc(_timeService.DateTimeUtcNow) - _timeService.UnityTimeNow), ErrorValue); + Assert.LessOrEqual(Math.Abs(_timeService.UnityTimeFromUnixTime(_timeService.UnixTimeNow) - _timeService.UnityTimeNow), ErrorValue); } [Test] - // ADMIT: TimeService.UnixTimeFromDateTimeUtc returns milliseconds since the Unix epoch, the unit UnixTimeNow - // reports in. - // RCR: TimeService.cs UnixTimeFromDateTimeUtc — add +100000L to the result → RED. NOTE as with the sibling - // above, the assertion is one-sided: switching TotalMilliseconds to TotalSeconds (a 1000x SHRINK) was observed - // to stay GREEN. Both fixtures want a two-sided bound on the absolute difference. + // ADMIT: TimeService.UnixTimeFromDateTimeUtc returns MILLISECONDS since the epoch, the unit UnixTimeNow + // reports in — hence UnixErrorMillis rather than the seconds-scale ErrorValue. + // RCR: TimeService.cs UnixTimeFromDateTimeUtc — return TotalSeconds instead of TotalMilliseconds → RED + // (isolated). That 1000x shrink passed under the previous one-sided assertion. public void UnixTime_Convertions_Successfully() { - Assert.GreaterOrEqual(ErrorValue, _timeService.UnixTimeFromDateTimeUtc(_timeService.DateTimeUtcNow) - _timeService.UnixTimeNow); - Assert.GreaterOrEqual(ErrorValue, _timeService.UnixTimeFromUnityTime(_timeService.UnityTimeNow) - _timeService.UnixTimeNow); + Assert.LessOrEqual(Math.Abs(_timeService.UnixTimeFromDateTimeUtc(_timeService.DateTimeUtcNow) - _timeService.UnixTimeNow), UnixErrorMillis); + Assert.LessOrEqual(Math.Abs(_timeService.UnixTimeFromUnityTime(_timeService.UnityTimeNow) - _timeService.UnixTimeNow), UnixErrorMillis); } [Test] From 08bfe69bb6c7cd0842132af144a1fbf6ca54f908 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 01:22:02 +0100 Subject: [PATCH 17/32] test: strengthen nine services tests that asserted nothing observable Five AssetResolverServiceTest cases asserted only Assert.DoesNotThrow. They now read the internal AssetMap - already exposed via InternalsVisibleTo, so no production change was needed - and assert the entries that were actually registered, merged or released. Subscribe_SameSubscriberSameType_LastActionWins_ThenUnsubscribeRemovesOnlyIt (renamed) gains a bystander subscriber on the same message type that must survive the Unsubscribe. Without it the "strengthened" test was still an A5 duplicate: Subscribe_MultipleSubscriptionSameType_ ReplacePreviousSubscription already covered every assertion, and its committed RCR names the same mutation verbatim. The bystander is what makes the pin unique. Worth recording, because it nearly produced a wrong result: the intended anchor `subscriptionObjects.Remove(subscriber);` matches TWICE - not because the sites are identical but because the UnsubscribeAll occurrence is indented one level deeper, making the 3-tab string a substring of the 4-tab line. A uniqueness check on the short string passes by eye and still mutates the wrong site. The spec now uses a multi-line anchor. RCR: AddAsset_NewType_RegistersEntry <- AssetResolverService.cs AddAssets store null instead of assets[i].Value RCR: AddAssets_DuplicateType_MergesEntries <- AssetResolverService.cs AddAssets delete the merge-loop Add RCR: UnloadAssets_WithAssetConfigsContainer_ReleasesAssetsInContainer <- AssetResolverService.cs delete dictionary.Remove(pair.Key) RCR: UnloadAssets_WithIdsArray_ReleasesOnlyMatching <- AssetResolverService.cs dictionary.Remove(id) to Clear() RCR: AddConfigs_DelegatesToAddAssets <- IAssetAdderService default method forwards an empty list RCR: Subscribe_SameSubscriberSameType_LastActionWins_ThenUnsubscribeRemovesOnlyIt <- MessageBrokerService.cs Unsubscribe Remove(subscriber) to Clear() RCR: ReplaceData_Successfully <- DataService.cs AddOrReplaceData neuter the replace branch RCR: VersionExternal_AlwaysAccessible_WithoutLoad <- VersionServices.cs VersionExternal return string.Empty RCR: Despawn_WithCondition_FirstOnly_Successfully <- ObjectPool.cs Despawn invert if (onlyFirst) The last one closes a gap this audit proved: the onlyFirst branch was uncovered repo-wide because both fixtures' predicates matched a single entity. The predicate now matches both. EditMode 806/806, PlayMode 295/295. Co-Authored-By: Claude Opus 5 --- .../EditMode/Unit/AssetResolverServiceTest.cs | 66 +++++++++++++++---- Tests/EditMode/Unit/DataServiceTest.cs | 16 +++-- .../EditMode/Unit/MessageBrokerServiceTest.cs | 21 ++++-- .../Unit/VersionServicesSyncLoadTest.cs | 10 +-- .../PlayMode/Unit/GameObjectPoolTypedTest.cs | 7 +- 5 files changed, 94 insertions(+), 26 deletions(-) diff --git a/Tests/EditMode/Unit/AssetResolverServiceTest.cs b/Tests/EditMode/Unit/AssetResolverServiceTest.cs index 29d983a..1d0baca 100644 --- a/Tests/EditMode/Unit/AssetResolverServiceTest.cs +++ b/Tests/EditMode/Unit/AssetResolverServiceTest.cs @@ -27,13 +27,27 @@ public void Init() } [Test] + // ADMIT: AssetResolverService.AddAssets must store the caller's AssetReference under its id in the + // per-asset-type map that every RequestAsset / UnloadAssets lookup reads. + // RCR: AssetResolverService.cs AddAssets — store `null` instead of `assets[i].Value` → RED (AreSame + // fails). Broad: also reddens the other AssetMap assertions in this fixture. 2026-08-04 public void AddAsset_NewType_RegistersEntry() { var assetRef = new AssetReference(); - Assert.DoesNotThrow(() => _service.AddAsset(typeof(Sprite), 1, assetRef)); + + _service.AddAsset(typeof(Sprite), 1, assetRef); + + var map = (Dictionary) _service.AssetMap[typeof(Sprite)][typeof(int)]; + + Assert.AreEqual(1, map.Count); + Assert.AreSame(assetRef, map[1]); } [Test] + // ADMIT: AssetResolverService.AddAssets merges a second registration for the same (assetType, idType) + // pair into the existing map instead of replacing it. + // RCR: AssetResolverService.cs AddAssets — delete `assetReferences.Add(asset.Key, asset.Value);` from the + // merge loop → RED (Count is 1 and ref1 is gone). Unique: no other test reaches the merge branch. 2026-08-04 public void AddAssets_DuplicateType_MergesEntries() { var ref1 = new AssetReference(); @@ -48,8 +62,11 @@ public void AddAssets_DuplicateType_MergesEntries() new Pair(2, ref2) }); - // Both entries should now exist — verifiable via UnloadAssets without throwing - Assert.DoesNotThrow(() => _service.UnloadAssets(false)); + var map = (Dictionary) _service.AssetMap[typeof(Sprite)][typeof(int)]; + + Assert.AreEqual(2, map.Count); + Assert.AreSame(ref1, map[1]); + Assert.AreSame(ref2, map[2]); } [Test] @@ -119,22 +136,41 @@ public void AddDebugConfigs_StoresAllProvided() } [Test] + // ADMIT: AssetResolverService.UnloadAssets(bool, AssetConfigsScriptableObject) clears only the ids + // the container lists, leaving every other registered id in the map. + // RCR: AssetResolverService.cs UnloadAssets(bool, AssetConfigsScriptableObject) — delete + // `dictionary.Remove(pair.Key);` → RED (Count is 3, not 1). Unique to that overload. 2026-08-04 public void UnloadAssets_WithAssetConfigsContainer_ReleasesAssetsInContainer() { + _service.AddAssets(typeof(Sprite), new List> + { + new Pair(1, new AssetReference()), + new Pair(2, new AssetReference()), + new Pair(3, new AssetReference()) + }); + var so = ScriptableObject.CreateInstance(); so.Configs = new List> { new Pair(1, new AssetReference()), new Pair(2, new AssetReference()) }; - _service.AddAssets(typeof(Sprite), so.Configs); - Assert.DoesNotThrow(() => _service.UnloadAssets(clearReferences: false, assetConfigs: so)); + _service.UnloadAssets(clearReferences: true, assetConfigs: so); + + var map = (Dictionary) _service.AssetMap[typeof(Sprite)][typeof(int)]; + + Assert.AreEqual(1, map.Count); + Assert.IsTrue(map.ContainsKey(3)); UnityEngine.Object.DestroyImmediate(so); } [Test] + // ADMIT: AssetResolverService.UnloadAssets(bool, params TId[]) removes only the listed ids from the + // id map rather than clearing it wholesale. + // RCR: AssetResolverService.cs UnloadAssets(bool, params TId[]) — change `dictionary.Remove(id);` to + // `dictionary.Clear();` → RED (Count is 0, not 1). Unique to that overload. 2026-08-04 public void UnloadAssets_WithIdsArray_ReleasesOnlyMatching() { _service.AddAssets(typeof(Sprite), new List> @@ -144,13 +180,19 @@ public void UnloadAssets_WithIdsArray_ReleasesOnlyMatching() new Pair(30, new AssetReference()) }); - Assert.DoesNotThrow(() => _service.UnloadAssets(clearReferences: true, 10, 20)); + _service.UnloadAssets(clearReferences: true, 10, 20); + + var map = (Dictionary) _service.AssetMap[typeof(Sprite)][typeof(int)]; - // The non-matching id 30 must still be resolvable — a second clear on the remaining map entries should not warn - Assert.DoesNotThrow(() => _service.UnloadAssets(clearReferences: true, 30)); + Assert.AreEqual(1, map.Count); + Assert.IsTrue(map.ContainsKey(30)); } [Test] + // ADMIT: IAssetAdderService.AddConfigs, a C# 8 default interface method, forwards the container's + // own Configs list to AssetResolverService.AddAssets. + // RCR: AssetResolverService.cs IAssetAdderService.AddConfigs — forward an empty list instead of + // `configs.Configs` → RED (Count is 0, not 1). Unique: only this test goes through the default method. 2026-08-04 public void AddConfigs_DelegatesToAddAssets() { var so = ScriptableObject.CreateInstance(); @@ -160,10 +202,12 @@ public void AddConfigs_DelegatesToAddAssets() }; IAssetAdderService adderService = _service; - Assert.DoesNotThrow(() => adderService.AddConfigs(so)); + adderService.AddConfigs(so); + + var map = (Dictionary) _service.AssetMap[typeof(Sprite)][typeof(int)]; - // Registered via the default interface method — subsequent unload should not warn - Assert.DoesNotThrow(() => _service.UnloadAssets(clearReferences: true)); + Assert.AreEqual(1, map.Count); + Assert.IsTrue(map.ContainsKey(7)); UnityEngine.Object.DestroyImmediate(so); } diff --git a/Tests/EditMode/Unit/DataServiceTest.cs b/Tests/EditMode/Unit/DataServiceTest.cs index f548c5c..cd06786 100644 --- a/Tests/EditMode/Unit/DataServiceTest.cs +++ b/Tests/EditMode/Unit/DataServiceTest.cs @@ -72,16 +72,20 @@ public void AddData_Successfully() } [Test] + // ADMIT: DataService.AddOrReplaceData overwrites the existing entry for typeof(T) on the replace branch rather + // than keeping the first instance. + // RCR: DataService.cs AddOrReplaceData — neuter the replace branch to `_data[typeof(T)] = _data[typeof(T)];` → + // RED (GetData still returns `first`). Also reddens SaveAllData_Successfully, which replaces the same type. + // 2026-08-04 public void ReplaceData_Successfully() { - var data = Substitute.For(); - var data1 = new object(); + var first = Substitute.For(); + var second = Substitute.For(); - _dataService.AddOrReplaceData(data1); - _dataService.AddOrReplaceData(data); + _dataService.AddOrReplaceData(first); + _dataService.AddOrReplaceData(second); - Assert.AreNotSame(data1, _dataService.GetData()); - Assert.AreSame(data, _dataService.GetData()); + Assert.AreSame(second, _dataService.GetData()); } [Test] diff --git a/Tests/EditMode/Unit/MessageBrokerServiceTest.cs b/Tests/EditMode/Unit/MessageBrokerServiceTest.cs index d50be23..3da7e2c 100644 --- a/Tests/EditMode/Unit/MessageBrokerServiceTest.cs +++ b/Tests/EditMode/Unit/MessageBrokerServiceTest.cs @@ -131,16 +131,29 @@ public void Unsubscribe_Successfully() } [Test] - public void UnsubscribeWithAction_MultipleSubscriptionSameType_RemoveAllScriptionsOfSameType() + // ADMIT: Subscribe keys by action.Target so the same subscriber's second action replaces its first, and + // Unsubscribe(subscriber) then removes only that subscriber's entry from the message type's bucket. + // RCR: MessageBrokerService.cs Unsubscribe — `subscriptionObjects.Remove(subscriber);` → + // `subscriptionObjects.Clear();` → RED (bystander.Received(2) sees 1). Isolated: Unsubscribe_Successfully has + // a single subscriber, for which Clear and Remove are indistinguishable. 2026-08-04 + public void Subscribe_SameSubscriberSameType_LastActionWins_ThenUnsubscribeRemovesOnlyIt() { + var bystander = Substitute.For(); + _messageBroker.Subscribe(_subscriber.MockMessageCall); _messageBroker.Subscribe(_subscriber.MockMessageCall2); - _messageBroker.Unsubscribe(_subscriber); + _messageBroker.Subscribe(bystander.MockMessageCall); + _messageBroker.Publish(_messageType1); - _messageBroker.PublishSafe(_messageType1); _subscriber.DidNotReceive().MockMessageCall(_messageType1); - _subscriber.DidNotReceive().MockMessageCall2(_messageType1); + _subscriber.Received(1).MockMessageCall2(_messageType1); + + _messageBroker.Unsubscribe(_subscriber); + _messageBroker.Publish(_messageType1); + + _subscriber.Received(1).MockMessageCall2(_messageType1); + bystander.Received(2).MockMessageCall(_messageType1); } [Test] diff --git a/Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs b/Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs index 093f3cd..72301a4 100644 --- a/Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs +++ b/Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs @@ -1,6 +1,7 @@ using System.Reflection; using GameLovers.Services; using NUnit.Framework; +using UnityEngine; // ReSharper disable once CheckNamespace @@ -118,12 +119,13 @@ public void AfterLoad_BuildNumber_ReturnsNonEmptyString() } [Test] + // ADMIT: VersionServices.VersionExternal must forward Application.version verbatim, with no dependency on the + // version-data resource having been loaded. + // RCR: VersionServices.cs VersionExternal — `=> string.Empty;` → RED (AreEqual reports the project version + // against ""). Unique: VersionInternal has its own fallback path and its own tests. 2026-08-04 public void VersionExternal_AlwaysAccessible_WithoutLoad() { - var external = VersionServices.VersionExternal; - - Assert.IsNotNull(external); - Assert.IsNotEmpty(external); + Assert.AreEqual(Application.version, VersionServices.VersionExternal); } } } diff --git a/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs b/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs index d833a2c..e794626 100644 --- a/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs +++ b/Tests/PlayMode/Unit/GameObjectPoolTypedTest.cs @@ -195,12 +195,17 @@ public IEnumerator IsSpawned_ReturnsTrueWhenMatch() } [UnityTest] + // ADMIT: ObjectPoolBase.Despawn(bool, Func) stops after the first match when onlyFirst is set, so a + // predicate matching every spawned entity still despawns exactly one. + // RCR: ObjectPool.cs Despawn(bool, Func) — neuter the `if (onlyFirst)` break guard to `if (false)` → RED (both + // entities despawn: count 0 and instance2 inactive). Isolated: + // Despawn_WithCondition_AllMatching_DespawnsAll passes onlyFirst: false and stays green. 2026-08-04 public IEnumerator Despawn_WithCondition_FirstOnly_Successfully() { var instance1 = _pool.Spawn(); var instance2 = _pool.Spawn(); - Assert.IsTrue(_pool.Despawn(onlyFirst: true, e => e == instance1)); + Assert.IsTrue(_pool.Despawn(onlyFirst: true, e => true)); Assert.AreEqual(1, _pool.SpawnedReadOnly.Count); Assert.IsFalse(instance1.gameObject.activeSelf); Assert.IsTrue(instance2.gameObject.activeSelf); From 90f3ea4e00683a17c318f15321142ed423ad79d2 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 01:37:45 +0100 Subject: [PATCH 18/32] docs: close the TimeService shrink-direction OPEN row 07abdf1 replaced the one-sided GreaterOrEqual(ErrorValue, converted - now) bounds with two-sided bounds on the absolute difference. The 1000x TotalMilliseconds -> TotalSeconds shrink and the _initialUnityTime negation, both previously observed GREEN, are now RED. The unix comparisons carry a separate UnixErrorMillis because they are in milliseconds while ErrorValue is in seconds. Co-Authored-By: Claude Opus 5 --- Tests/AGENTS.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index e3ebbcd..dd55f11 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -322,9 +322,8 @@ The count of OPEN rows is the honest coverage-debt number. | `ServicesScaffolders` `#if UNITY_6000_4_OR_NEWER` guard (`Editor/Scaffolders/ServicesScaffolders.cs`) | ACCEPTED | (iii) harness-impossible — blocker: compile-time branch, only one side is reachable per Unity version, so a single test run can only ever exercise one branch of the `#if`. | 2026-07-31 | | `VersionServices.IsOutdatedVersion` (`Runtime/VersionServices.cs`) | OPEN | Owed: only coverage was a local reimplementation of the algorithm in `VersionServicesTest.cs`; a real test against the actual method is owed. | 2026-07-31 | | `AddressableIdsGeneratorUtils.ResolveSanitizedEnumName` 3-way collision (`Editor/AddressableIds/AddressableIdsGeneratorUtils.cs:531-539`) | OPEN | The two-address collision was fixed 2026-08-01 (`AppendAddressEnumMembers` now emits the disambiguated `name`). A deeper edge case remains: a THIRD address colliding with the same base name AND filetype re-derives the identical `"{name}_{filetype}"` suffix (the fallback only ever adds one suffix level and the collision check is against the original `name`, not against previously-suffixed candidates), so 3+ colliding addresses can still emit duplicates. Owed: either a numeric fallback (`_2`, `_3`, ...) or checking collision against the full history of emitted names, not just base names. | 2026-08-01 | - | `ObjectPoolBase.Despawn` `onlyFirst` break (`Runtime/Pooling/ObjectPool.cs`) | OPEN | Owed: the branch is uncovered **repo-wide**, proven not inferred — inverting `if (onlyFirst)` left `Despawn_WithCondition_FirstOnly_Successfully` GREEN (the EditMode fixture spawns one entity; the PlayMode typed sibling's predicate matches only one). A test must spawn two matching entities and assert exactly one is despawned. | 2026-08-04 | -| `TimeService.UnityTimeFromDateTimeUtc` / `UnixTimeFromDateTimeUtc` shrink direction (`Runtime/TimeService.cs`) | OPEN | Owed: both fixtures assert `GreaterOrEqual(ErrorValue, converted - now)`, which bounds the difference only from ABOVE. Negating `_initialUnityTime`, and swapping `TotalMilliseconds` for `TotalSeconds` (a 1000x shrink), were both observed GREEN. Needs a two-sided bound on the absolute difference. | 2026-08-04 | +| `TimeService.UnityTimeFromDateTimeUtc` / `UnixTimeFromDateTimeUtc` shrink direction (`Runtime/TimeService.cs`) | CLOSED | Closed 2026-08-04 by `07abdf1`: the three assertion pairs now bound `Math.Abs(converted - now)` from both sides. The 1000x `TotalMilliseconds`→`TotalSeconds` shrink and the `_initialUnityTime` negation are now both observed RED (unix comparisons use a separate `UnixErrorMillis`, since they are in milliseconds). | 2026-08-04 | | `RngService.Peekfloat` state-advance detection (`Runtime/RngService.cs`) | OPEN | Owed: `Peekfloat` draws over `(0, floatP.MaxValue)`, where consecutive draws saturate to the same `floatP` — so making `PeekRange` consume the LIVE state was observed GREEN. The test is precision-blind to the advance its name claims to catch. | 2026-08-04 | ## 14. Update Policy From a5b1bb7e60bbd041190f2e719b903970a390cc89 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 12:40:27 +0100 Subject: [PATCH 19/32] =?UTF-8?q?docs:=20add=20CLOSED=20as=20a=20=C2=A713?= =?UTF-8?q?=20state=20and=20require=20re-derivation=20to=20close=20a=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §13 asserted every named symbol was 'either ACCEPTED or OPEN' while four packages had already grown CLOSED rows — the spec forbade rows it contained. CLOSED is now first-class, and it carries a contract: name the commit AND the observation, including the environment the observation came from. A row closed on 'the fix landed' is still OPEN, because the fix is the edit and the closure is the evidence. Second rule: closing a row means re-deriving its claim against current source, never reading the commit that claimed to fix it. A partial fix and a complete one produce the same green suite and the same confident commit message, so the commit cannot be evidence for its own completeness. §1 and §2 are shared verbatim across all six packages; §13's preamble is too. Co-Authored-By: Claude Opus 5 --- Tests/AGENTS.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index dd55f11..4c1782b 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -293,9 +293,24 @@ confirming `MathfloatP` reports ~1002 coverable lines — a smaller figure means `-debugCodeOptimization` was missing and the denominator silently shrank ~40%. -Every untested symbol worth naming is either ACCEPTED (justified — do not -re-report) or OPEN (a real gap, owed a test). An untested symbol in neither state -is an audit finding. +Every untested symbol worth naming is ACCEPTED (justified — do not re-report), +OPEN (a real gap, owed a test), or CLOSED (the gap was filled). An untested symbol +in none of the three is an audit finding. + +**A CLOSED row must name the commit AND the observation that closed it, including the +environment the observation came from.** A row closed on "the fix landed" is still OPEN: +the fix is the edit, the closure is the evidence. This is what kept the uiservice A6 row +open until the Editor half ran — the edit was in and batchmode was green, and neither of +those was the thing in doubt. + +**Closing a row means re-deriving its claim against current source, never reading the +commit that claimed to fix it.** Re-check every symbol and fixture the row names. A partial +fix and a complete one produce the same green suite and the same confident commit message, +so the commit cannot be the evidence for its own completeness. Recorded instance: the +mobileservices editor-static row nearly closed on a commit that genuinely did stop fixtures +inheriting statics — for two of the three fixtures the row named. The third was found by +grepping which fixtures touch each static, and it was passing only because its siblings +happened to restore the static in their `finally` blocks. An ACCEPTED row needs one of exactly three falsifiable reasons: - **(i) no branching** — zero conditionals, so there is no behaviour to pin. From dede053050b0b9249c2c81438fb18a2338855524 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 12:57:09 +0100 Subject: [PATCH 20/32] test: write back 3 RCR annotations whose observation was recorded but never applied Same recovery as the gamedata commit: probed and observed RED during the backfill, but the annotate step never reached the source. Gated on a recorded RED-OK verdict per test. Note Despawn_WithCondition_FirstOnly_Successfully carries both a RED-OK and a STAYED-GREEN observation, from two different mutations. The annotation records the one that reddens it (the break body returning false); the section 13 OPEN row about the onlyFirst inversion staying green is a separate, still-owed gap and is untouched. Comment-only diff. Co-Authored-By: Claude Opus 5 --- Tests/EditMode/Unit/ObjectPoolTest.cs | 8 ++++++++ Tests/PlayMode/Unit/GameObjectPoolTest.cs | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/Tests/EditMode/Unit/ObjectPoolTest.cs b/Tests/EditMode/Unit/ObjectPoolTest.cs index 75334a2..15d8418 100644 --- a/Tests/EditMode/Unit/ObjectPoolTest.cs +++ b/Tests/EditMode/Unit/ObjectPoolTest.cs @@ -208,6 +208,10 @@ public void IsSpawned_ReturnsTrueWhenMatch() } [Test] + // ADMIT: ObjectPoolBase.Despawn(onlyFirst, predicate) must still report true when it stops after the + // first match, not swallow the result on the early exit. + // RCR: ObjectPool.cs Despawn(bool, Func) — the `if (onlyFirst) { break; }` body → `return false;` → RED + // (Assert.IsTrue fails even though the entity was despawned). 2026-08-02 public void Despawn_WithCondition_FirstOnly_Successfully() { var entity = _pool.Spawn(); @@ -229,6 +233,10 @@ public void Despawn_WithCondition_NoMatch_ReturnsFalse() } [Test] + // ADMIT: ObjectPoolBase.Despawn(onlyFirst: false, predicate) must keep scanning after the first match + // instead of exiting the loop. + // RCR: ObjectPool.cs Despawn(bool, Func) — make the break unconditional (`if (onlyFirst)` → `if (true)`) → + // RED (SpawnedReadOnly.Count is 1, not 0). 2026-08-02 public void Despawn_WithCondition_AllMatching_DespawnsAll() { _pool.Spawn(); diff --git a/Tests/PlayMode/Unit/GameObjectPoolTest.cs b/Tests/PlayMode/Unit/GameObjectPoolTest.cs index eb1f4e7..36e5956 100644 --- a/Tests/PlayMode/Unit/GameObjectPoolTest.cs +++ b/Tests/PlayMode/Unit/GameObjectPoolTest.cs @@ -147,6 +147,10 @@ public IEnumerator SpawnWithData_InvokesIPoolEntitySpawn() } [UnityTest] + // ADMIT: GameObjectPool.Dispose must use the Unity fake-null guard (`obj == null`) on every Clear() entry — + // a pooled instance can be destroyed by an external parent while the pool still tracks it. + // RCR: GameObjectPool.cs GameObjectPool.Dispose() — `if (obj == null)` → `if (obj.transform == null)` → RED + // (MissingReferenceException from dereferencing the destroyed GameObject). 2026-08-02 public IEnumerator Dispose_AfterDespawnedInstanceDestroyedExternally_DoesNotThrow() { var externalParent = new GameObject("ExternalParent"); From a9097eb5db8a4f78a9633dd1d10b34d94f786a05 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 15:09:33 +0100 Subject: [PATCH 21/32] docs+test: Smoke RCR exemption, the three unannotated states, and the unowned-edit measurement Tests/AGENTS.md section 2 said a test with no // RCR: line is a suspect by default, without carving out Smoke/. Section 1 already exempts that directory (its defect class is "the assembly no longer loads", which has no one-line mutation), so the omission flagged those fixtures forever. Exemption is now explicit, on the same directory basis. Also records that "unannotated" is three states, not one: observed RED with the write-back lost, seen reddening only as collateral, or never probed. Only the last needs a probe, and prepared annotation text must never be written without a matching RED-OK - it exists for tests that were never probed, and writing it fabricates a verified claim. Adds a section 13 row for the measured count of production edits that redden only collaterally (223 repo-wide, from .test-all/rcr/unowned-edits.json). Recorded with the caveat that it is NOT that many missing tests: for foundational primitives and the UiService integration hub, having no isolated owner follows from centrality, not neglect. --- Tests/AGENTS.md | 14 +++++++++++++- Tests/PlayMode/Unit/TickServiceTest.cs | 5 +++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index 4c1782b..7812d9f 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -185,7 +185,17 @@ symbol appears anywhere in the causal chain behind the assertion. **Two consequences, stated so RCR does not become theatre:** - A test with no `// RCR:` line — and no UNFALSIFIABLE exemption — is not trusted - coverage. In an audit it is a suspect by default. + coverage. In an audit it is a suspect by default. **`Smoke/` is exempt here too**, on the + same directory basis as §1: its defect class is "the assembly no longer loads", which has + no one-line mutation, so demanding an RCR line there flags those fixtures forever. The + exemption is the directory, not the assertion shape. +- **"Unannotated" is three states, not one, and they need different actions.** A test with no + `// RCR:` line may have been (a) observed RED with the write-back lost, (b) seen reddening + only as collateral inside another test's blast radius, or (c) never probed. Only (c) needs a + probe; (a) needs the recorded observation written back; (b) is SHARED-PATH evidence, not a + unique pin. Check `.test-all/rcr/` before probing, and never write prepared annotation text + without a matching `RED-OK` for that test — prepared text also exists for tests that were + never probed, and writing it fabricates a verified claim. - **Benchmarks are included, inverted:** a performance test must be observed *changing its number* when the measured operation is removed from the measured body. A benchmark whose measured region does not contain the workload is a @@ -340,6 +350,8 @@ The count of OPEN rows is the honest coverage-debt number. | `ObjectPoolBase.Despawn` `onlyFirst` break (`Runtime/Pooling/ObjectPool.cs`) | OPEN | Owed: the branch is uncovered **repo-wide**, proven not inferred — inverting `if (onlyFirst)` left `Despawn_WithCondition_FirstOnly_Successfully` GREEN (the EditMode fixture spawns one entity; the PlayMode typed sibling's predicate matches only one). A test must spawn two matching entities and assert exactly one is despawned. | 2026-08-04 | | `TimeService.UnityTimeFromDateTimeUtc` / `UnixTimeFromDateTimeUtc` shrink direction (`Runtime/TimeService.cs`) | CLOSED | Closed 2026-08-04 by `07abdf1`: the three assertion pairs now bound `Math.Abs(converted - now)` from both sides. The 1000x `TotalMilliseconds`→`TotalSeconds` shrink and the `_initialUnityTime` negation are now both observed RED (unix comparisons use a separate `UnixErrorMillis`, since they are in milliseconds). | 2026-08-04 | | `RngService.Peekfloat` state-advance detection (`Runtime/RngService.cs`) | OPEN | Owed: `Peekfloat` draws over `(0, floatP.MaxValue)`, where consecutive draws saturate to the same `floatP` — so making `PeekRange` consume the LIVE state was observed GREEN. The test is precision-blind to the advance its name claims to catch. | 2026-08-04 | +| 64 production edits reddening only collaterally (`Runtime/RngService.cs`, `Runtime/CoroutineService.cs`, `Runtime/TickService.cs`) | OPEN | Measured 2026-08-04 from `.test-all/rcr/unowned-edits.json`: 64 edits produced RED but never an `isolated` verdict — `RngService.cs` (11), `CoroutineService.cs` (9), `TickService.cs` (7), `ObjectPool.cs` (6). **Not 64 missing tests**; see the gamedata row for the reasoning. Owed: a judgement pass on whether the deterministic-RNG and tick-fan-out paths warrant narrow pins, since both are shared setup for many fixtures. | 2026-08-04 | +| `TickServiceTest.MultipleInstances_CreateMultipleGameObjects` exemption is an unfinished RCR (`Runtime/TickService.cs`) | OPEN | Found 2026-08-04 by the same adversarial pass. The exemption claimed *"only a structural change (making the guard static) flips it"*, but `TickService.cs` declares `private readonly TickServiceMonoBehaviour _tickObject;` — changing that one line to `private static TickServiceMonoBehaviour _tickObject;` makes the ctor's `_tickObject != null` guard bite, so the second construction adds no host and the count assertion goes RED. A one-line mutation exists and was never run. Comment corrected to name it. Owed: run it. | 2026-08-04 | ## 14. Update Policy Update this file when: diff --git a/Tests/PlayMode/Unit/TickServiceTest.cs b/Tests/PlayMode/Unit/TickServiceTest.cs index 5f1bce0..a82ec9e 100644 --- a/Tests/PlayMode/Unit/TickServiceTest.cs +++ b/Tests/PlayMode/Unit/TickServiceTest.cs @@ -441,8 +441,9 @@ public IEnumerator Unsubscribe_UmbrellaOverload_RemovesActionFromAllThreeUpdateL [Test] // ADMIT: TickService does NOT enforce a singleton -- the ctor's `_tickObject != null` guard reads an instance // field and is therefore always false, so each construction adds its own TickServiceMonoBehaviour. - // RCR: none exists -- the assertion pins the ABSENCE of singleton enforcement; only a structural change - // (making the guard static) flips it. Unfalsifiable one line at a time, not a duplicate. + // RCR: TickService.cs -- `private readonly TickServiceMonoBehaviour _tickObject;` -> `private static + // TickServiceMonoBehaviour _tickObject;` makes the ctor guard bite, so the second construction adds no + // host and the count assertion goes RED. OWED: that mutation is one line and has not been run. public void MultipleInstances_CreateMultipleGameObjects() { // Note: The service doesn't enforce singleton, but it throws if _tickObject is already set From 809b5b579bcd69f7f944be601aa1d920db1ff134 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 15:40:13 +0100 Subject: [PATCH 22/32] =?UTF-8?q?docs:=20align=20XML=20doc=20comments=20wi?= =?UTF-8?q?th=20AGENTS.md=20=C2=A76.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical pass, no behaviour change. - Removed the AssetReferenceScene constructor's doc block (§6.6 never XML-documents constructors). - Converted the doc comments on 12 private members to `//` comments rather than deleting them, so the rationale survives where §6.6 forbids `///` on private code. Two carry real knowledge worth keeping: ObjectPoolBase.IsDestroyedOrNull's explanation of why `entity == null` cannot see a destroyed UnityEngine.Object through a `class`-only generic constraint, and VersionServices.Bootstrap's note that ordering between SubsystemRegistration callbacks across assemblies is undefined. - Added `/// ` to 41 overrides whose base declaration is already documented: the Explorer tab BuildUi/Refresh/OnExitingPlayMode family, and Equals/GetHashCode on TickData. - Moved the internal AddressableIdsGeneratorUtils.BuildEnumSource and AssetResolverService.SelectAsset test seams above their types' private blocks (§6.6: `internal` is never interleaved with `private`), and removed the change-narration sentences from both summaries — "Extracted from …; no behaviour change versus the logic it replaces" is diff context, which §6.6's Code comments rule forbids. Verified: Tools/style-audit.py reports 0 remaining mechanical-class violations here; brace and paren counts are unchanged against HEAD on every touched file (including the two whose members moved); and every file's UTF-8 BOM still matches HEAD, after an intermediate pass stripped two of them. The 5 findings left are overrides whose own base declarations are undocumented (GameObjectPool.SpawnEntity/PostDespawnEntity, AsyncCoroutine.OnCompleteTrigger); `` there would yield empty IntelliSense, so they are handled with their bases in the prose pass. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 3 + .../AddressableIdsGeneratorUtils.cs | 56 ++++++-------- Editor/Explorer/Tabs/AddressableIdsTab.cs | 2 + Editor/Explorer/Tabs/AssetResolverTab.cs | 13 ++-- Editor/Explorer/Tabs/AssetsImporterTab.cs | 2 + Editor/Explorer/Tabs/CoroutineTab.cs | 3 + Editor/Explorer/Tabs/DataTab.cs | 3 + Editor/Explorer/Tabs/InstallerTab.cs | 3 + Editor/Explorer/Tabs/MessageBrokerTab.cs | 14 ++-- Editor/Explorer/Tabs/OverviewTab.cs | 2 + Editor/Explorer/Tabs/PoolTab.cs | 3 + Editor/Explorer/Tabs/RngTab.cs | 3 + Editor/Explorer/Tabs/TickTab.cs | 3 + Editor/Explorer/Tabs/TimeTab.cs | 2 + Editor/Explorer/Tabs/VersioningTab.cs | 9 +-- .../AssetConfigsScriptableObjectEditor.cs | 1 + .../Inspectors/AssetReferenceSceneDrawer.cs | 1 + Editor/Scaffolders/ServicesScaffolders.cs | 4 + Editor/Versioning/GitEditorProcess.cs | 4 +- Editor/Versioning/VersionEditorUtils.cs | 10 +-- Editor/Versioning/VersioningMenu.cs | 6 +- Runtime/AssetResolverService.cs | 75 ++++++++++--------- Runtime/AssetsImporter/AssetReferenceScene.cs | 4 - Runtime/Pooling/ObjectPool.cs | 14 ++-- Runtime/RngService.cs | 4 +- Runtime/TickService.cs | 2 + Runtime/VersionServices.cs | 20 ++--- 27 files changed, 135 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2019aff..cc87efb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +**Docs**: +- Aligned XML doc comments with the host repo's `AGENTS.md` §6.6. Removed the `AssetReferenceScene` constructor's doc block, and converted the doc comments on 12 private members to `//` comments so their rationale survives where §6.6 forbids `///` on private code — notably `ObjectPoolBase.IsDestroyedOrNull`'s fake-null explanation and `VersionServices.Bootstrap`'s `SubsystemRegistration` ordering note. Added `/// ` to 41 overrides whose base documentation already exists (the Explorer tab `BuildUi`/`Refresh` family, plus `Equals`/`GetHashCode` on `TickData`). Moved the internal `AddressableIdsGeneratorUtils.BuildEnumSource` and `AssetResolverService.SelectAsset` test seams above their types' private blocks, per §6.6's rule that `internal` is never interleaved with `private`, and dropped the change-narration sentences from both summaries ("Extracted from …; no behaviour change versus the logic it replaces") which §6.6 forbids. + **Fixed**: - `GameObjectPool.Dispose(bool disposeSampleEntity)` and `GameObjectPool.Dispose(bool)` destroyed `SampleEntity` unconditionally, ignoring the argument — `Dispose(false)` destroyed a sample entity the caller explicitly asked to keep. - `ObjectPoolBase.SpawnEntity` could hand out a destroyed pooled object. Its retry loop tested `entity == null`, which inside a generic constrained only to `class` compiles to plain reference equality and never reaches `UnityEngine.Object`'s overloaded `==` that detects a destroyed ("fake-null") native object. A pooled `GameObject`/`Behaviour` destroyed by an external owner while despawned was therefore returned to the next `Spawn()` caller, who hit a `MissingReferenceException` a frame later. Now routed through a runtime type check that dispatches to the Unity overload when the entity is a `UnityEngine.Object`, and falls back to reference equality for POCO pooled types. diff --git a/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs b/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs index e30fbd7..6b7dd46 100644 --- a/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs +++ b/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs @@ -241,6 +241,24 @@ public static string ResolveScriptPath(AddressableIdsEditorSettings settings) return scriptPath; } + /// + /// Pure string-builder: turns into the generated C# enum member-block + /// source text (opening brace, one member per address, closing brace) that + /// inserts after the public enum <Name> header line. Takes no AssetDatabase + /// dependency — operates on plain address strings only, so it is directly unit-testable. + /// + internal static string BuildEnumSource(IReadOnlyCollection addresses) + { + var addressList = addresses as IReadOnlyList ?? new List(addresses); + var stringBuilder = new StringBuilder(); + + stringBuilder.AppendLine("\t{"); + AppendAddressEnumMembers(stringBuilder, addressList); + stringBuilder.AppendLine("\t}"); + + return stringBuilder.ToString(); + } + private static List GetAssetList() { var assetList = new List(); @@ -483,26 +501,6 @@ private static void ProcessData(IList assetList, Addressa } } - /// - /// Pure string-builder: turns into the generated C# enum member-block - /// source text (opening brace, one member per address, closing brace) that - /// inserts after the public enum <Name> header line. Takes no AssetDatabase - /// dependency — operates on plain address strings only, so it is directly unit-testable. - /// Extracted from the previous inlined GenerateAddressEnums(StringBuilder, IReadOnlyList<AddressableAssetEntry>); - /// no behaviour change versus the logic it replaces. - /// - internal static string BuildEnumSource(IReadOnlyCollection addresses) - { - var addressList = addresses as IReadOnlyList ?? new List(addresses); - var stringBuilder = new StringBuilder(); - - stringBuilder.AppendLine("\t{"); - AppendAddressEnumMembers(stringBuilder, addressList); - stringBuilder.AppendLine("\t}"); - - return stringBuilder.ToString(); - } - private static void AppendAddressEnumMembers(StringBuilder stringBuilder, IReadOnlyList addresses) { var addedNames = new List(); @@ -518,14 +516,10 @@ private static void AppendAddressEnumMembers(StringBuilder stringBuilder, IReadO } } - /// - /// Resolves the name_filetype-suffixed disambiguation candidate for a given Addressable - /// when its cleaned name collides with one already in - /// . Sets to true when the fallback - /// path was taken. Used by both (to emit a unique enum member - /// name) and (to report collisions for the Explorer diff - /// view). - /// + // Resolves the name_filetype-suffixed disambiguation candidate for a given Addressable address when its + // cleaned name collides with one already in seenNames. Sets collided to true when the fallback path was + // taken. Used by both AppendAddressEnumMembers (to emit a unique enum member name) and + // DetectSanitizedNameCollisions (to report collisions for the Explorer diff view). private static string ResolveSanitizedEnumName(string address, List seenNames, out bool collided) { var name = GetCleanName(address, true); @@ -586,10 +580,8 @@ private static List DetectNullAssetTypes(IReadOnlyList - /// Returns the elements of that are not in . - /// Both inputs MUST be pre-sorted ordinally; output is also sorted ordinally. - /// + // Returns the elements of left that are not in right. Both inputs MUST be pre-sorted ordinally; output is + // also sorted ordinally. private static List SortedSetDiff(IReadOnlyList left, IReadOnlyList right) { var result = new List(); diff --git a/Editor/Explorer/Tabs/AddressableIdsTab.cs b/Editor/Explorer/Tabs/AddressableIdsTab.cs index 1d5fc22..785fbf4 100644 --- a/Editor/Explorer/Tabs/AddressableIdsTab.cs +++ b/Editor/Explorer/Tabs/AddressableIdsTab.cs @@ -43,6 +43,7 @@ public class AddressableIdsTab : ServiceTab private VisualElement _removedList; private VisualElement _warningsList; + /// protected override void BuildUi() { var scroll = new ScrollView(ScrollViewMode.Vertical); @@ -174,6 +175,7 @@ protected override void BuildUi() RefreshOutput(); } + /// protected override void Refresh() { var settings = AddressableIdsEditorSettings.instance; diff --git a/Editor/Explorer/Tabs/AssetResolverTab.cs b/Editor/Explorer/Tabs/AssetResolverTab.cs index ab9d284..b87b76b 100644 --- a/Editor/Explorer/Tabs/AssetResolverTab.cs +++ b/Editor/Explorer/Tabs/AssetResolverTab.cs @@ -22,6 +22,7 @@ public class AssetResolverTab : ServiceTab private Toggle _destructiveToggle; private Label _countLabel; + /// protected override void BuildUi() { var header = new VisualElement(); @@ -51,6 +52,7 @@ protected override void BuildUi() Add(bar); } + /// protected override void Refresh() { var resolver = TryResolve() as AssetResolverService; @@ -136,13 +138,10 @@ protected override void Refresh() } } - /// - /// Builds a deterministic digest of every piece of state the rebuild path renders: - /// the not-bound branch, the destructive-toggle flag (gates per-row Unload buttons), - /// and per-row (assetType, idType, id, loaded) tuples. When two consecutive - /// refreshes produce the same digest the rebuild can be skipped — keeping rapid - /// foldout clicks from getting destroyed mid-click by the 250 ms timer. - /// + // Builds a deterministic digest of every piece of state the rebuild path renders: the not-bound branch, the + // destructive-toggle flag (gates per-row Unload buttons), and per-row (assetType, idType, id, loaded) tuples. + // When two consecutive refreshes produce the same digest the rebuild can be skipped — keeping rapid foldout + // clicks from getting destroyed mid-click by the 250 ms timer. private string ComputeDigest(AssetResolverService resolver) { if (resolver == null) diff --git a/Editor/Explorer/Tabs/AssetsImporterTab.cs b/Editor/Explorer/Tabs/AssetsImporterTab.cs index fa6b305..30596da 100644 --- a/Editor/Explorer/Tabs/AssetsImporterTab.cs +++ b/Editor/Explorer/Tabs/AssetsImporterTab.cs @@ -22,6 +22,7 @@ public class AssetsImporterTab : ServiceTab private VisualElement _importerList; private List _cachedImporters; + /// protected override void BuildUi() { var scroll = new ScrollView(ScrollViewMode.Vertical); @@ -54,6 +55,7 @@ protected override void BuildUi() Add(bar); } + /// protected override void Refresh() { _autoImportToggle.SetValueWithoutNotify(AssetsImporterEditorSettings.instance.AutoUpdateOnRefresh); diff --git a/Editor/Explorer/Tabs/CoroutineTab.cs b/Editor/Explorer/Tabs/CoroutineTab.cs index 0f33d6c..52ee075 100644 --- a/Editor/Explorer/Tabs/CoroutineTab.cs +++ b/Editor/Explorer/Tabs/CoroutineTab.cs @@ -14,6 +14,7 @@ public class CoroutineTab : ServiceTab private VisualElement _list; private Label _totalLabel; + /// protected override void BuildUi() { var header = new VisualElement(); @@ -34,6 +35,7 @@ protected override void BuildUi() Add(bar); } + /// protected override void Refresh() { _list.Clear(); @@ -87,6 +89,7 @@ protected override void Refresh() // Forcibly clear the active-coroutine list synchronously when the user stops // play mode. Belt-and-braces against bootstraps that fail to dispose the // coroutine service / call MainInstaller.Clean() in OnDestroy. + /// protected override void OnExitingPlayMode() { _totalLabel.text = "Active: 0"; diff --git a/Editor/Explorer/Tabs/DataTab.cs b/Editor/Explorer/Tabs/DataTab.cs index 6c49df8..a8cd274 100644 --- a/Editor/Explorer/Tabs/DataTab.cs +++ b/Editor/Explorer/Tabs/DataTab.cs @@ -19,6 +19,7 @@ public class DataTab : ServiceTab private VisualElement _list; private Label _countLabel; + /// protected override void BuildUi() { var header = new VisualElement(); @@ -39,6 +40,7 @@ protected override void BuildUi() Add(bar); } + /// protected override void Refresh() { _list.Clear(); @@ -112,6 +114,7 @@ protected override void Refresh() // mode. Belt-and-braces against bootstraps that fail to call MainInstaller.Clean() // in OnDestroy — the DataService entries dictionary lives on the service instance // and would otherwise surface as a stale snapshot in edit mode. + /// protected override void OnExitingPlayMode() { _countLabel.text = "Entries: 0"; diff --git a/Editor/Explorer/Tabs/InstallerTab.cs b/Editor/Explorer/Tabs/InstallerTab.cs index a508d35..6e86b6a 100644 --- a/Editor/Explorer/Tabs/InstallerTab.cs +++ b/Editor/Explorer/Tabs/InstallerTab.cs @@ -17,6 +17,7 @@ public class InstallerTab : ServiceTab private VisualElement _list; private VisualElement _actionBar; + /// protected override void BuildUi() { _scroll = new ScrollView(ScrollViewMode.Vertical); @@ -31,6 +32,7 @@ protected override void BuildUi() Add(_actionBar); } + /// protected override void Refresh() { _list.Clear(); @@ -85,6 +87,7 @@ protected override void Refresh() // Belt-and-braces against bootstraps that fail to call MainInstaller.Clean() in // OnDestroy — without this the static MainInstaller would surface stale bindings // in the tab until the next play session. + /// protected override void OnExitingPlayMode() { _list.Clear(); diff --git a/Editor/Explorer/Tabs/MessageBrokerTab.cs b/Editor/Explorer/Tabs/MessageBrokerTab.cs index 38d25e7..347aab4 100644 --- a/Editor/Explorer/Tabs/MessageBrokerTab.cs +++ b/Editor/Explorer/Tabs/MessageBrokerTab.cs @@ -25,6 +25,7 @@ public class MessageBrokerTab : ServiceTab private readonly List _messageTypes = new List(); private int _lastDiscoveredAssemblyCount = -1; + /// protected override void BuildUi() { _scroll = new ScrollView(ScrollViewMode.Vertical); @@ -69,6 +70,7 @@ protected override void BuildUi() RebuildMessageTypeChoices(); } + /// protected override void Refresh() { RebuildMessageTypeChoicesIfStale(); @@ -82,6 +84,7 @@ protected override void Refresh() // reset (next domain reload) without this guard. Note: ServiceTab also invalidates // the refresh digest on play-mode transitions, so the deferred refresh that lands // after scene teardown will rebuild from scratch instead of short-circuiting. + /// protected override void OnExitingPlayMode() { _list.Clear(); @@ -173,13 +176,10 @@ private void RefreshSubscriptionList() } } - /// - /// Builds a deterministic digest of every piece of state the rebuild path renders: - /// edit-mode-empty, not-bound, and per-subscription (messageType, [target.method, ...]) - /// tuples. When two consecutive refreshes produce the same digest the rebuild can be - /// skipped — keeping rapid foldout clicks from getting destroyed mid-click by the - /// 250 ms timer. - /// + // Builds a deterministic digest of every piece of state the rebuild path renders: edit-mode-empty, not-bound, + // and per-subscription (messageType, [target.method, ...]) tuples. When two consecutive refreshes produce the + // same digest the rebuild can be skipped — keeping rapid foldout clicks from getting destroyed mid-click by + // the 250 ms timer. private static string ComputeDigest(bool isPlaying, MessageBrokerService broker) { if (!isPlaying) diff --git a/Editor/Explorer/Tabs/OverviewTab.cs b/Editor/Explorer/Tabs/OverviewTab.cs index 81fa618..994ee40 100644 --- a/Editor/Explorer/Tabs/OverviewTab.cs +++ b/Editor/Explorer/Tabs/OverviewTab.cs @@ -27,6 +27,7 @@ public OverviewTab(ServicesExplorerWindow window) _window = window; } + /// protected override void BuildUi() { var scroll = new ScrollView(ScrollViewMode.Vertical); @@ -39,6 +40,7 @@ protected override void BuildUi() Add(scroll); } + /// protected override void Refresh() { _grid.Clear(); diff --git a/Editor/Explorer/Tabs/PoolTab.cs b/Editor/Explorer/Tabs/PoolTab.cs index 5d784c7..880d98c 100644 --- a/Editor/Explorer/Tabs/PoolTab.cs +++ b/Editor/Explorer/Tabs/PoolTab.cs @@ -18,6 +18,7 @@ public class PoolTab : ServiceTab private VisualElement _list; private Label _countLabel; + /// protected override void BuildUi() { var header = new VisualElement(); @@ -38,6 +39,7 @@ protected override void BuildUi() Add(bar); } + /// protected override void Refresh() { _list.Clear(); @@ -175,6 +177,7 @@ private void OnRemovePool(PoolService poolService, Type entityType) // MainInstaller.Clean() in OnDestroy — without this the static MainInstaller // would surface stale pools (and their spawned-readonly counts) until the next // play session. + /// protected override void OnExitingPlayMode() { _countLabel.text = "Pools: 0"; diff --git a/Editor/Explorer/Tabs/RngTab.cs b/Editor/Explorer/Tabs/RngTab.cs index e9d1a81..fb3b9af 100644 --- a/Editor/Explorer/Tabs/RngTab.cs +++ b/Editor/Explorer/Tabs/RngTab.cs @@ -34,6 +34,7 @@ public class RngTab : ServiceTab private SliderInt _restoreSlider; private IntegerField _restoreCountField; + /// protected override void BuildUi() { var scroll = new ScrollView(ScrollViewMode.Vertical); @@ -160,6 +161,7 @@ protected override void BuildUi() Add(scroll); } + /// protected override void Refresh() { // Hide RNG state in edit mode (initial OR after a play session ended) regardless @@ -189,6 +191,7 @@ protected override void Refresh() // Forcibly clear all RNG-state widgets when the user stops play mode. Belt-and-braces // guarantee that the tab does not retain a frozen "last play" snapshot even if the // consumer's bootstrap forgot to call MainInstaller.Clean() in OnDestroy. + /// protected override void OnExitingPlayMode() { ShowUnboundState(); diff --git a/Editor/Explorer/Tabs/TickTab.cs b/Editor/Explorer/Tabs/TickTab.cs index ec7c97c..939c559 100644 --- a/Editor/Explorer/Tabs/TickTab.cs +++ b/Editor/Explorer/Tabs/TickTab.cs @@ -15,6 +15,7 @@ public class TickTab : ServiceTab private Foldout _lateFoldout; private VisualElement _actionBar; + /// protected override void BuildUi() { var scroll = new ScrollView(ScrollViewMode.Vertical); @@ -42,6 +43,7 @@ protected override void BuildUi() Add(_actionBar); } + /// protected override void Refresh() { // Hide tick subscriber lists in edit mode regardless of any leftover @@ -74,6 +76,7 @@ protected override void Refresh() // Forcibly clear all three tick subscriber lists synchronously when the user // stops play mode. Belt-and-braces against bootstraps that fail to dispose // ITickService / call MainInstaller.Clean() in OnDestroy. + /// protected override void OnExitingPlayMode() { ShowEmptyState(); diff --git a/Editor/Explorer/Tabs/TimeTab.cs b/Editor/Explorer/Tabs/TimeTab.cs index f876c84..f31c35e 100644 --- a/Editor/Explorer/Tabs/TimeTab.cs +++ b/Editor/Explorer/Tabs/TimeTab.cs @@ -24,6 +24,7 @@ public class TimeTab : ServiceTab private TextField _setInitialField; private VisualElement _manipulatorSection; + /// protected override void BuildUi() { var scroll = new ScrollView(ScrollViewMode.Vertical); @@ -92,6 +93,7 @@ protected override void BuildUi() Add(bar); } + /// protected override void Refresh() { var service = TryResolve(); diff --git a/Editor/Explorer/Tabs/VersioningTab.cs b/Editor/Explorer/Tabs/VersioningTab.cs index 87a2532..81907a7 100644 --- a/Editor/Explorer/Tabs/VersioningTab.cs +++ b/Editor/Explorer/Tabs/VersioningTab.cs @@ -36,6 +36,7 @@ private static string VersionDataFilePath } } + /// protected override void BuildUi() { var scroll = new ScrollView(ScrollViewMode.Vertical); @@ -73,6 +74,7 @@ protected override void BuildUi() Add(scroll); } + /// protected override void Refresh() { _externalLabel.text = VersionServices.VersionExternal; @@ -193,11 +195,8 @@ private void OnRevealFile() } } - /// - /// Returns a forward-slash project-relative path (e.g. Assets/Configs/Resources) - /// from an absolute path that lives under . - /// Returns the original string unchanged if it does not start with . - /// + // Returns a forward-slash project-relative path (e.g. Assets/Configs/Resources) from an absolute path that + // lives under baseDir. Returns the original string unchanged if it does not start with baseDir. private static string GetRelativePath(string baseDir, string fullPath) { var normalBase = baseDir.Replace('\\', '/').TrimEnd('/') + '/'; diff --git a/Editor/Inspectors/AssetConfigsScriptableObjectEditor.cs b/Editor/Inspectors/AssetConfigsScriptableObjectEditor.cs index 3210f37..176d191 100644 --- a/Editor/Inspectors/AssetConfigsScriptableObjectEditor.cs +++ b/Editor/Inspectors/AssetConfigsScriptableObjectEditor.cs @@ -20,6 +20,7 @@ public class AssetConfigsScriptableObjectEditor : UnityEditor.Editor private VisualElement _diagnosticsPanel; private Label _diagnosticsLabel; + /// public override VisualElement CreateInspectorGUI() { var root = new VisualElement(); diff --git a/Editor/Inspectors/AssetReferenceSceneDrawer.cs b/Editor/Inspectors/AssetReferenceSceneDrawer.cs index 40faabb..5e55197 100644 --- a/Editor/Inspectors/AssetReferenceSceneDrawer.cs +++ b/Editor/Inspectors/AssetReferenceSceneDrawer.cs @@ -14,6 +14,7 @@ namespace GameLovers.Services.Inspectors.Editor [CustomPropertyDrawer(typeof(AssetReferenceScene), useForChildren: true)] public class AssetReferenceSceneDrawer : PropertyDrawer { + /// public override VisualElement CreatePropertyGUI(SerializedProperty property) { var container = new VisualElement(); diff --git a/Editor/Scaffolders/ServicesScaffolders.cs b/Editor/Scaffolders/ServicesScaffolders.cs index 6118a1b..3adae0b 100644 --- a/Editor/Scaffolders/ServicesScaffolders.cs +++ b/Editor/Scaffolders/ServicesScaffolders.cs @@ -106,11 +106,13 @@ private class ScriptNameEditAction : AssetCreationEndAction { public string TemplatePath; + /// public override void Action(EntityId entityId, string pathName, string resourceFile) { WriteScript(pathName); } + /// public override void Cancelled(EntityId entityId, string pathName, string resourceFile) { } @@ -179,11 +181,13 @@ private class ScriptNameEditAction : EndNameEditAction { public string TemplatePath; + /// public override void Action(int instanceId, string pathName, string resourceFile) { WriteScript(pathName); } + /// public override void Cancelled(int instanceId, string pathName, string resourceFile) { } diff --git a/Editor/Versioning/GitEditorProcess.cs b/Editor/Versioning/GitEditorProcess.cs index 03d059e..0b66847 100644 --- a/Editor/Versioning/GitEditorProcess.cs +++ b/Editor/Versioning/GitEditorProcess.cs @@ -73,9 +73,7 @@ public void Dispose() Process?.Dispose(); } - /// - /// Execute a command eg. "status --verbose" - /// + // Execute a command eg. "status --verbose" private string ExecuteCommand(string args) { Process.StartInfo.Arguments = args; diff --git a/Editor/Versioning/VersionEditorUtils.cs b/Editor/Versioning/VersionEditorUtils.cs index 405c2a1..6f9f92b 100644 --- a/Editor/Versioning/VersionEditorUtils.cs +++ b/Editor/Versioning/VersionEditorUtils.cs @@ -48,9 +48,7 @@ public static string LoadVersionDataSerializedSync() return serialized; } - /// - /// Set the internal version for when the app plays in editor. - /// + // Set the internal version for when the app plays in editor. [InitializeOnLoadMethod] private static void OnEditorLoad() { @@ -105,10 +103,8 @@ private static VersionServices.VersionData GenerateInternalVersionSuffix(bool is return data; } - /// - /// Set the internal version of this application and save it in resources. This should be - /// called at edit/build time. - /// + // Set the internal version of this application and save it in resources. This should be called at edit/build + // time. private static void SaveVersionData(string serializedData) { var relFolderPath = VersioningEditorSettings.instance.ResourcesFolderPath; diff --git a/Editor/Versioning/VersioningMenu.cs b/Editor/Versioning/VersioningMenu.cs index 934c8c7..5c31ee1 100644 --- a/Editor/Versioning/VersioningMenu.cs +++ b/Editor/Versioning/VersioningMenu.cs @@ -10,10 +10,8 @@ namespace GameLovers.Services.Versioning.Editor /// internal static class VersioningMenu { - /// - /// Regenerates version-data.txt from the current git state (non-store build). - /// Equivalent to the domain-reload trigger — useful after branch switches without a full reload. - /// + // Regenerates version-data.txt from the current git state (non-store build). Equivalent to the domain-reload + // trigger — useful after branch switches without a full reload. [MenuItem("Tools/GameLovers/Versioning/Refresh Version Data", priority = 100)] private static void Refresh() => VersionEditorUtils.SetAndSaveInternalVersion(false); diff --git a/Runtime/AssetResolverService.cs b/Runtime/AssetResolverService.cs index 72cab75..a708c01 100644 --- a/Runtime/AssetResolverService.cs +++ b/Runtime/AssetResolverService.cs @@ -413,6 +413,42 @@ public void AddDebugConfigs(Sprite errorSprite = null, GameObject errorCube = nu _errorClip = errorClip; } + internal static TAsset SelectAsset(Type type, Object asset, bool isDone, bool instantiate, + Sprite errorSprite, GameObject errorCube, Material errorMaterial, AudioClip errorClip) + where TAsset : Object + { + if (asset == null) + { + return null; + } + + if (type == typeof(GameObject)) + { + var selected = !isDone ? errorCube : asset as GameObject; + + return instantiate ? Object.Instantiate(selected) as TAsset : selected as TAsset; + } + + if (type == typeof(Sprite)) + { + return !isDone ? errorSprite as TAsset : asset as TAsset; + } + + if (type == typeof(Material)) + { + var selected = !isDone ? errorMaterial : asset as Material; + + return instantiate ? new Material(selected) as TAsset : selected as TAsset; + } + + if (type == typeof(AudioClip)) + { + return !isDone ? errorClip as TAsset : asset as TAsset; + } + + return asset as TAsset; + } + private TAsset Convert(AssetReference assetReference, bool instantiate) where TAsset : Object { @@ -424,8 +460,8 @@ private TAsset Convert(AssetReference assetReference, bool instantiate) /// Pure type-switch that resolves the to return for a given Addressables /// / pair, substituting the matching error placeholder /// (///) - /// when the reference has not finished loading. Extracted from for direct - /// testability; no behaviour change versus the inlined switch it replaces. + /// when the reference has not finished loading. Kept separate from so the + /// type-switch is directly testable. /// /* * AssetReference types @@ -461,41 +497,6 @@ private TAsset Convert(AssetReference assetReference, bool instantiate) Scene GUISkin * */ - internal static TAsset SelectAsset(Type type, Object asset, bool isDone, bool instantiate, - Sprite errorSprite, GameObject errorCube, Material errorMaterial, AudioClip errorClip) - where TAsset : Object - { - if (asset == null) - { - return null; - } - - if (type == typeof(GameObject)) - { - var selected = !isDone ? errorCube : asset as GameObject; - - return instantiate ? Object.Instantiate(selected) as TAsset : selected as TAsset; - } - - if (type == typeof(Sprite)) - { - return !isDone ? errorSprite as TAsset : asset as TAsset; - } - - if (type == typeof(Material)) - { - var selected = !isDone ? errorMaterial : asset as Material; - - return instantiate ? new Material(selected) as TAsset : selected as TAsset; - } - - if (type == typeof(AudioClip)) - { - return !isDone ? errorClip as TAsset : asset as TAsset; - } - - return asset as TAsset; - } private bool TryGetDictionary(out Dictionary dictionary) { diff --git a/Runtime/AssetsImporter/AssetReferenceScene.cs b/Runtime/AssetsImporter/AssetReferenceScene.cs index ec91942..daf1622 100644 --- a/Runtime/AssetsImporter/AssetReferenceScene.cs +++ b/Runtime/AssetsImporter/AssetReferenceScene.cs @@ -11,10 +11,6 @@ namespace GameLovers.Services.AssetsImporter [System.Serializable] public class AssetReferenceScene : AssetReference { - /// - /// Construct a new AssetReference object. - /// - /// The guid of the asset. public AssetReferenceScene(string guid) : base(guid) { } diff --git a/Runtime/Pooling/ObjectPool.cs b/Runtime/Pooling/ObjectPool.cs index 7dd3b36..0b3d7f3 100644 --- a/Runtime/Pooling/ObjectPool.cs +++ b/Runtime/Pooling/ObjectPool.cs @@ -213,15 +213,11 @@ protected virtual void CallOnDespawned(T entity) poolEntity?.OnDespawn(); } - /// - /// A plain entity == null inside this generic class only ever performs C# reference-equality: - /// is constrained to class, not to UnityEngine.Object, so the - /// compiler cannot dispatch to UnityEngine.Object's overloaded == that detects a - /// destroyed-but-not-null ("fake-null") native object. When 's runtime type - /// IS a UnityEngine.Object (e.g. a pooled GameObject/Behaviour), this dispatches to - /// that overload via a runtime type check instead; for a non-Unity (a POCO - /// pooled type), it falls back to a plain reference-null check. - /// + // A plain entity == null inside this generic class only ever performs C# reference-equality: T is constrained + // to class, not to UnityEngine.Object, so the compiler cannot dispatch to UnityEngine.Object's overloaded == + // that detects a destroyed-but-not-null ("fake-null") native object. When entity's runtime type IS a + // UnityEngine.Object (e.g. a pooled GameObject/Behaviour), this dispatches to that overload via a runtime + // type check instead; for a non-Unity T (a POCO pooled type), it falls back to a plain reference-null check. private static bool IsDestroyedOrNull(T entity) { return entity is UnityEngine.Object unityObject ? unityObject == null : entity == null; diff --git a/Runtime/RngService.cs b/Runtime/RngService.cs index 75c30ba..54a66fb 100644 --- a/Runtime/RngService.cs +++ b/Runtime/RngService.cs @@ -321,9 +321,7 @@ public static int[] GenerateRngState(int seed) return state; } - /// - /// Generates the next random number between [0...int.MaxValue] based on the given - /// + // Generates the next random number between [0...int.MaxValue] based on the given rndState private static int NextNumber(int[] rndState) { var index1 = rndState[_valueIndex] + 1; diff --git a/Runtime/TickService.cs b/Runtime/TickService.cs index 4606ded..ebf27d9 100644 --- a/Runtime/TickService.cs +++ b/Runtime/TickService.cs @@ -381,11 +381,13 @@ public bool Equals(TickData other) return other.Id == Id; } + /// public override bool Equals(object other) { return other is TickData && Equals((TickData)other); } + /// public override int GetHashCode() { return Id; diff --git a/Runtime/VersionServices.cs b/Runtime/VersionServices.cs index b50dcd1..a90b4a6 100644 --- a/Runtime/VersionServices.cs +++ b/Runtime/VersionServices.cs @@ -87,19 +87,13 @@ public static string BuildNumber private static VersionData _versionData; private static bool _loaded; - /// - /// Auto-bootstrap hook: populates version metadata at the earliest runtime phase Unity - /// exposes, before any scene Awake and before vendor SDK SubsystemRegistration - /// callbacks that read / (e.g. - /// Sentry's Option Config Script). Consumers no longer need to call - /// / explicitly for the - /// default flow. - /// - /// - /// Ordering between callbacks - /// across assemblies is undefined; if a sibling SDK's hook fires before this one, the - /// property accessors' lazy-load fallback () covers the race. - /// + // Auto-bootstrap hook: populates version metadata at the earliest runtime phase Unity exposes, before any + // scene Awake and before vendor SDK SubsystemRegistration callbacks that read VersionInternal / BuildNumber + // (e.g. Sentry's Option Config Script). Consumers no longer need to call LoadVersionData / + // LoadVersionDataAsync explicitly for the default flow. Ordering between + // RuntimeInitializeLoadType.SubsystemRegistration callbacks across assemblies is undefined; if a sibling + // SDK's hook fires before this one, the property accessors' lazy-load fallback (EnsureLoaded) covers the + // race. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void Bootstrap() { From f440d49136a8401e5895c557f4df95d1c5635b8a Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 15:50:20 +0100 Subject: [PATCH 23/32] test: run the TickService singleton mutation the exemption said did not exist MultipleInstances_CreateMultipleGameObjects carried "// RCR: none exists -- only a structural change (making the guard static) flips it". That was wrong: TickService.cs declares `private readonly TickServiceMonoBehaviour _tickObject;` and changing that one line to `private static TickServiceMonoBehaviour _tickObject;` is a single-line edit. Run: the test went RED with production's own InvalidOperationException, "The tick service is being initialized for the second time and that is not valid". TickService.cs restored byte-identical. Also removes 15 lines from the test body: a narration block ("Wait, I saw a check in the constructor:") wrapping a commented-out copy of the production constructor. The rationale it carried is real and now lives in the one-line ADMIT above; the commented-out code was dead. Records a new section 13 row for what the mutation exposed: that ctor guard is unreachable. It reads a readonly INSTANCE field assigned later in the same constructor, so it is always null when checked, and its message promises singleton protection the package deliberately does not provide (see the package AGENTS.md section 4). Deleting the dead guard is owed but is a production edit outside this pass. RCR: MultipleInstances_CreateMultipleGameObjects <- TickService.cs _tickObject readonly -> static Editor PlayMode 295/295. Co-Authored-By: Claude Opus 5 --- Tests/AGENTS.md | 3 ++- Tests/PlayMode/Unit/TickServiceTest.cs | 20 ++------------------ 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index 7812d9f..d6128d8 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -351,7 +351,8 @@ The count of OPEN rows is the honest coverage-debt number. | `TimeService.UnityTimeFromDateTimeUtc` / `UnixTimeFromDateTimeUtc` shrink direction (`Runtime/TimeService.cs`) | CLOSED | Closed 2026-08-04 by `07abdf1`: the three assertion pairs now bound `Math.Abs(converted - now)` from both sides. The 1000x `TotalMilliseconds`→`TotalSeconds` shrink and the `_initialUnityTime` negation are now both observed RED (unix comparisons use a separate `UnixErrorMillis`, since they are in milliseconds). | 2026-08-04 | | `RngService.Peekfloat` state-advance detection (`Runtime/RngService.cs`) | OPEN | Owed: `Peekfloat` draws over `(0, floatP.MaxValue)`, where consecutive draws saturate to the same `floatP` — so making `PeekRange` consume the LIVE state was observed GREEN. The test is precision-blind to the advance its name claims to catch. | 2026-08-04 | | 64 production edits reddening only collaterally (`Runtime/RngService.cs`, `Runtime/CoroutineService.cs`, `Runtime/TickService.cs`) | OPEN | Measured 2026-08-04 from `.test-all/rcr/unowned-edits.json`: 64 edits produced RED but never an `isolated` verdict — `RngService.cs` (11), `CoroutineService.cs` (9), `TickService.cs` (7), `ObjectPool.cs` (6). **Not 64 missing tests**; see the gamedata row for the reasoning. Owed: a judgement pass on whether the deterministic-RNG and tick-fan-out paths warrant narrow pins, since both are shared setup for many fixtures. | 2026-08-04 | -| `TickServiceTest.MultipleInstances_CreateMultipleGameObjects` exemption is an unfinished RCR (`Runtime/TickService.cs`) | OPEN | Found 2026-08-04 by the same adversarial pass. The exemption claimed *"only a structural change (making the guard static) flips it"*, but `TickService.cs` declares `private readonly TickServiceMonoBehaviour _tickObject;` — changing that one line to `private static TickServiceMonoBehaviour _tickObject;` makes the ctor's `_tickObject != null` guard bite, so the second construction adds no host and the count assertion goes RED. A one-line mutation exists and was never run. Comment corrected to name it. Owed: run it. | 2026-08-04 | +| `TickServiceTest.MultipleInstances_CreateMultipleGameObjects` exemption is an unfinished RCR (`Runtime/TickService.cs`) | CLOSED | Closed 2026-08-04. The one-line mutation exists and was run: `private readonly TickServiceMonoBehaviour _tickObject;` -> `private static ...` makes production's own ctor guard fire, and the test went RED with `InvalidOperationException: The tick service is being initialized for the second time`. Observed in the **Editor**; `TickService.cs` restored byte-identical. Also removed 15 lines of narration and commented-out production code from the test body. See the new dead-guard row below. | 2026-08-04 | +| `TickService` ctor singleton guard is unreachable dead code (`Runtime/TickService.cs`) | OPEN | Found 2026-08-04 while running the mutation above. The ctor opens with `if (_tickObject != null) throw new InvalidOperationException("...initialized for the second time...")`, but `_tickObject` is a `readonly` **instance** field assigned later in that same ctor, so it is always `null` at the guard — the guard can never fire and its message promises protection that does not exist. The package `AGENTS.md` §4 documents the opposite on purpose (*"These services do not enforce a singleton"*), and `MultipleInstances_CreateMultipleGameObjects` pins the no-singleton behaviour. Owed: delete the dead guard per the root `AGENTS.md` rule preferring deletion over speculative code. Not done here because it is a production edit outside this pass's scope, and deleting it removes the only thing the RCR mutation above leverages — the replacement mutation would need to be the field itself. | 2026-08-04 | ## 14. Update Policy Update this file when: diff --git a/Tests/PlayMode/Unit/TickServiceTest.cs b/Tests/PlayMode/Unit/TickServiceTest.cs index a82ec9e..5d9f6a7 100644 --- a/Tests/PlayMode/Unit/TickServiceTest.cs +++ b/Tests/PlayMode/Unit/TickServiceTest.cs @@ -442,26 +442,10 @@ public IEnumerator Unsubscribe_UmbrellaOverload_RemovesActionFromAllThreeUpdateL // ADMIT: TickService does NOT enforce a singleton -- the ctor's `_tickObject != null` guard reads an instance // field and is therefore always false, so each construction adds its own TickServiceMonoBehaviour. // RCR: TickService.cs -- `private readonly TickServiceMonoBehaviour _tickObject;` -> `private static - // TickServiceMonoBehaviour _tickObject;` makes the ctor guard bite, so the second construction adds no - // host and the count assertion goes RED. OWED: that mutation is one line and has not been run. + // TickServiceMonoBehaviour _tickObject;` -> RED (production's own ctor guard throws + // InvalidOperationException "initialized for the second time"). 2026-08-04 public void MultipleInstances_CreateMultipleGameObjects() { - // Note: The service doesn't enforce singleton, but it throws if _tickObject is already set - // However, _tickObject is an instance field in the current implementation. - // Wait, I saw a check in the constructor: - /* - public TickService() - { - if (_tickObject != null) - { - throw new InvalidOperationException("The tick service is being initialized for the second time and that is not valid"); - } - ... - } - */ - // But _tickObject is private readonly TickServiceMonoBehaviour _tickObject; - // So it's always null for a new instance. The check seems to be intended for a static field but isn't. - var service1 = new TickService(); var service2 = new TickService(); From a5a0a750e6a35efd63c8af04cc3016e062be1500 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 16:19:18 +0100 Subject: [PATCH 24/32] fix(tick): delete the unreachable singleton guard and re-point its test's RCR The TickService constructor opened with a guard that could never fire: if (_tickObject != null) throw new InvalidOperationException("...second time..."); _tickObject is a readonly INSTANCE field assigned later in that same constructor, so it is always null at the check. The guard advertised singleton protection the service deliberately does not provide - AGENTS.md section 4 documents multi-instance as the contract, and MultipleInstances_CreateMultipleGameObjects pins it. CoroutineService, which spawns its host the same way, never carried such a guard; deleting this one makes the pair consistent. Deleting it voided the test's RCR, so the mutation was re-derived rather than assumed: - Old mutation re-run WITH the guard gone: _tickObject readonly -> static left the test GREEN (observed, not inferred). Recorded on the test as a negative result so the next reader does not repeat it. - New mutation: the ctor's own AddComponent, routed through Object.FindAnyObjectByType() ?? gameObject.AddComponent<...>() - a simulated singleton - went RED with "Expected: greater than or equal to 2". Green -> RED -> green, TickService.cs restored to the guard-deleted state (md5 verified). RCR: MultipleInstances_CreateMultipleGameObjects <- TickService.cs ctor AddComponent -> reuse an existing host Batchmode EditMode 805/805, PlayMode 295/295. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 1 + Runtime/TickService.cs | 5 ----- Tests/AGENTS.md | 2 +- Tests/PlayMode/Unit/TickServiceTest.cs | 11 ++++++----- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc87efb..41afb40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Aligned XML doc comments with the host repo's `AGENTS.md` §6.6. Removed the `AssetReferenceScene` constructor's doc block, and converted the doc comments on 12 private members to `//` comments so their rationale survives where §6.6 forbids `///` on private code — notably `ObjectPoolBase.IsDestroyedOrNull`'s fake-null explanation and `VersionServices.Bootstrap`'s `SubsystemRegistration` ordering note. Added `/// ` to 41 overrides whose base documentation already exists (the Explorer tab `BuildUi`/`Refresh` family, plus `Equals`/`GetHashCode` on `TickData`). Moved the internal `AddressableIdsGeneratorUtils.BuildEnumSource` and `AssetResolverService.SelectAsset` test seams above their types' private blocks, per §6.6's rule that `internal` is never interleaved with `private`, and dropped the change-narration sentences from both summaries ("Extracted from …; no behaviour change versus the logic it replaces") which §6.6 forbids. **Fixed**: +- `TickService`'s constructor opened with an `if (_tickObject != null) throw new InvalidOperationException("...initialized for the second time...")` guard that could never fire: `_tickObject` is a `readonly` **instance** field assigned later in that same constructor, so it was always `null` when checked. The guard advertised singleton protection the service deliberately does not provide (see `AGENTS.md` §4 — constructing multiple instances creates multiple host GameObjects, and `TickServiceTest.MultipleInstances_CreateMultipleGameObjects` pins that). Removed; `CoroutineService`, which has the same host-spawning shape, never carried one. - `GameObjectPool.Dispose(bool disposeSampleEntity)` and `GameObjectPool.Dispose(bool)` destroyed `SampleEntity` unconditionally, ignoring the argument — `Dispose(false)` destroyed a sample entity the caller explicitly asked to keep. - `ObjectPoolBase.SpawnEntity` could hand out a destroyed pooled object. Its retry loop tested `entity == null`, which inside a generic constrained only to `class` compiles to plain reference equality and never reaches `UnityEngine.Object`'s overloaded `==` that detects a destroyed ("fake-null") native object. A pooled `GameObject`/`Behaviour` destroyed by an external owner while despawned was therefore returned to the next `Spawn()` caller, who hit a `MissingReferenceException` a frame later. Now routed through a runtime type check that dispatches to the Unity overload when the entity is a `UnityEngine.Object`, and falls back to reference equality for POCO pooled types. - `AddressableIdsGeneratorUtils` emitted duplicate enum members when two Addressable addresses sanitized to the same C# identifier (e.g. `ui/main-menu` and `ui-main/menu` both clean to `ui_main_menu`), producing generated code that does not compile. The member-append path now uses the disambiguated name it already computed instead of re-deriving the raw cleaned name. diff --git a/Runtime/TickService.cs b/Runtime/TickService.cs index ebf27d9..661886b 100644 --- a/Runtime/TickService.cs +++ b/Runtime/TickService.cs @@ -120,11 +120,6 @@ public class TickService : ITickService public TickService() { - if (_tickObject != null) - { - throw new InvalidOperationException("The tick service is being initialized for the second time and that is not valid"); - } - var gameObject = new GameObject(typeof(TickServiceMonoBehaviour).Name); Object.DontDestroyOnLoad(gameObject); diff --git a/Tests/AGENTS.md b/Tests/AGENTS.md index d6128d8..5f3a002 100644 --- a/Tests/AGENTS.md +++ b/Tests/AGENTS.md @@ -352,7 +352,7 @@ The count of OPEN rows is the honest coverage-debt number. | `RngService.Peekfloat` state-advance detection (`Runtime/RngService.cs`) | OPEN | Owed: `Peekfloat` draws over `(0, floatP.MaxValue)`, where consecutive draws saturate to the same `floatP` — so making `PeekRange` consume the LIVE state was observed GREEN. The test is precision-blind to the advance its name claims to catch. | 2026-08-04 | | 64 production edits reddening only collaterally (`Runtime/RngService.cs`, `Runtime/CoroutineService.cs`, `Runtime/TickService.cs`) | OPEN | Measured 2026-08-04 from `.test-all/rcr/unowned-edits.json`: 64 edits produced RED but never an `isolated` verdict — `RngService.cs` (11), `CoroutineService.cs` (9), `TickService.cs` (7), `ObjectPool.cs` (6). **Not 64 missing tests**; see the gamedata row for the reasoning. Owed: a judgement pass on whether the deterministic-RNG and tick-fan-out paths warrant narrow pins, since both are shared setup for many fixtures. | 2026-08-04 | | `TickServiceTest.MultipleInstances_CreateMultipleGameObjects` exemption is an unfinished RCR (`Runtime/TickService.cs`) | CLOSED | Closed 2026-08-04. The one-line mutation exists and was run: `private readonly TickServiceMonoBehaviour _tickObject;` -> `private static ...` makes production's own ctor guard fire, and the test went RED with `InvalidOperationException: The tick service is being initialized for the second time`. Observed in the **Editor**; `TickService.cs` restored byte-identical. Also removed 15 lines of narration and commented-out production code from the test body. See the new dead-guard row below. | 2026-08-04 | -| `TickService` ctor singleton guard is unreachable dead code (`Runtime/TickService.cs`) | OPEN | Found 2026-08-04 while running the mutation above. The ctor opens with `if (_tickObject != null) throw new InvalidOperationException("...initialized for the second time...")`, but `_tickObject` is a `readonly` **instance** field assigned later in that same ctor, so it is always `null` at the guard — the guard can never fire and its message promises protection that does not exist. The package `AGENTS.md` §4 documents the opposite on purpose (*"These services do not enforce a singleton"*), and `MultipleInstances_CreateMultipleGameObjects` pins the no-singleton behaviour. Owed: delete the dead guard per the root `AGENTS.md` rule preferring deletion over speculative code. Not done here because it is a production edit outside this pass's scope, and deleting it removes the only thing the RCR mutation above leverages — the replacement mutation would need to be the field itself. | 2026-08-04 | +| `TickService` ctor singleton guard is unreachable dead code (`Runtime/TickService.cs`) | CLOSED | Closed 2026-08-04: the dead guard is deleted (`CHANGELOG.md` **Fixed**), which also makes `TickService` consistent with `CoroutineService` — same host-spawning shape, never carried such a guard. `MultipleInstances_CreateMultipleGameObjects` was re-pointed, because removing the guard voided its previous mutation: with the guard gone, `_tickObject` `readonly`->`static` was observed leaving the test **GREEN**. The replacement pins the ctor's own `AddComponent` — routing it through `Object.FindAnyObjectByType() ?? ...` (a simulated singleton) was observed RED with "Expected: greater than or equal to 2". Green -> RED -> green, `TickService.cs` restored to the guard-deleted state (md5 verified). | 2026-08-04 | ## 14. Update Policy Update this file when: diff --git a/Tests/PlayMode/Unit/TickServiceTest.cs b/Tests/PlayMode/Unit/TickServiceTest.cs index 5d9f6a7..8aa1c10 100644 --- a/Tests/PlayMode/Unit/TickServiceTest.cs +++ b/Tests/PlayMode/Unit/TickServiceTest.cs @@ -439,11 +439,12 @@ public IEnumerator Unsubscribe_UmbrellaOverload_RemovesActionFromAllThreeUpdateL } [Test] - // ADMIT: TickService does NOT enforce a singleton -- the ctor's `_tickObject != null` guard reads an instance - // field and is therefore always false, so each construction adds its own TickServiceMonoBehaviour. - // RCR: TickService.cs -- `private readonly TickServiceMonoBehaviour _tickObject;` -> `private static - // TickServiceMonoBehaviour _tickObject;` -> RED (production's own ctor guard throws - // InvalidOperationException "initialized for the second time"). 2026-08-04 + // ADMIT: TickService deliberately does NOT enforce a singleton -- every construction adds its own + // TickServiceMonoBehaviour host, which is what lets two services tick independently. + // RCR: TickService.cs ctor -- `_tickObject = gameObject.AddComponent();` -> + // `... = Object.FindAnyObjectByType() ?? gameObject.AddComponent<...>();` + // -> RED ("Expected: greater than or equal to 2"). Negative result: making `_tickObject` static no + // longer reddens this, since the dead ctor guard that mutation relied on is gone. 2026-08-04 public void MultipleInstances_CreateMultipleGameObjects() { var service1 = new TickService(); From 3fe91277991a0a61c55dbcef6e1b9c0ce9c844b2 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 16:21:11 +0100 Subject: [PATCH 25/32] =?UTF-8?q?docs:=20hold=20private-member=20comments?= =?UTF-8?q?=20to=20the=20higher=20bar=20in=20AGENTS.md=20=C2=A76.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier §6.6 pass converted every private member's doc block into a `//` comment to avoid losing rationale. That was too generous: a comment on a private member is a last resort, and needing one is usually evidence the member is wrong. §6.6 now says so explicitly, and this applies it. Deleted outright — the name and signature already say it: - GitEditorProcess.ExecuteCommand ("Execute a command eg. status --verbose") - VersionEditorUtils.OnEditorLoad (an [InitializeOnLoadMethod] that sets the version — the attribute and body carry it) - VersionEditorUtils.SaveVersionData - RngService.NextNumber ("Generates the next random number ... based on rndState") - MobileSimulatorRuntimeOverlay.OverlayController, whose second sentence was also change narration about a window that no longer exists Reduced to only what the code cannot state: - SortedSetDiff -> its pre-sorted precondition, which no type enforces - ResolveSanitizedEnumName -> the name_filetype disambiguation strategy, dropping the "used by X and Y" call-graph narration §6.6 forbids in - GetRelativePath -> the silent unchanged-string fallback - VersioningMenu.Refresh -> what the `false` argument means - VersionServices.Bootstrap -> the cross-assembly SubsystemRegistration race and why EnsureLoaded is what closes it, dropping the consumer-facing sentence that belongs on the public API and is already in AGENTS.md §4 - AssetResolverTab / MessageBrokerTab digests -> a pointer at AGENTS.md §4, which documents the rapid-click rationale at length, instead of restating it Kept in full: ObjectPoolBase.IsDestroyedOrNull. A `class`-only generic constraint cannot dispatch to UnityEngine.Object's `==`, so the fake-null hazard is invisible in the code and nothing but prose can convey it. Net: 31 comment lines removed, 9 added. Verified after this pass: Tools/style-audit.py still reports 0 for every mechanical rule class, and batchmode is green — EditMode 805/805, PlayMode 295/295. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 +- Editor/AddressableIds/AddressableIdsGeneratorUtils.cs | 8 ++------ Editor/Explorer/Tabs/AssetResolverTab.cs | 5 +---- Editor/Explorer/Tabs/MessageBrokerTab.cs | 5 +---- Editor/Explorer/Tabs/VersioningTab.cs | 3 +-- Editor/Versioning/GitEditorProcess.cs | 1 - Editor/Versioning/VersionEditorUtils.cs | 3 --- Editor/Versioning/VersioningMenu.cs | 3 +-- Runtime/RngService.cs | 1 - Runtime/VersionServices.cs | 9 ++------- 10 files changed, 9 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41afb40..7e003f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] **Docs**: -- Aligned XML doc comments with the host repo's `AGENTS.md` §6.6. Removed the `AssetReferenceScene` constructor's doc block, and converted the doc comments on 12 private members to `//` comments so their rationale survives where §6.6 forbids `///` on private code — notably `ObjectPoolBase.IsDestroyedOrNull`'s fake-null explanation and `VersionServices.Bootstrap`'s `SubsystemRegistration` ordering note. Added `/// ` to 41 overrides whose base documentation already exists (the Explorer tab `BuildUi`/`Refresh` family, plus `Equals`/`GetHashCode` on `TickData`). Moved the internal `AddressableIdsGeneratorUtils.BuildEnumSource` and `AssetResolverService.SelectAsset` test seams above their types' private blocks, per §6.6's rule that `internal` is never interleaved with `private`, and dropped the change-narration sentences from both summaries ("Extracted from …; no behaviour change versus the logic it replaces") which §6.6 forbids. +- Aligned XML doc comments with the host repo's `AGENTS.md` §6.6. Removed the `AssetReferenceScene` constructor's doc block. Of the 12 doc comments on private members, the four that only restated the member's own name were deleted outright (`GitEditorProcess.ExecuteCommand`, `VersionEditorUtils.OnEditorLoad` / `SaveVersionData`, `RngService.NextNumber`); the rest were reduced to the part the code genuinely cannot state — `ObjectPoolBase.IsDestroyedOrNull`'s fake-null explanation kept in full, `VersionServices.Bootstrap` cut to the cross-assembly `SubsystemRegistration` race, `SortedSetDiff` to its unenforced pre-sorted precondition, and the two Explorer digest helpers to a pointer at this file's §4 rather than restating it. Added `/// ` to 41 overrides whose base documentation already exists (the Explorer tab `BuildUi`/`Refresh` family, plus `Equals`/`GetHashCode` on `TickData`). Moved the internal `AddressableIdsGeneratorUtils.BuildEnumSource` and `AssetResolverService.SelectAsset` test seams above their types' private blocks, per §6.6's rule that `internal` is never interleaved with `private`, and dropped the change-narration sentences from both summaries ("Extracted from …; no behaviour change versus the logic it replaces") which §6.6 forbids. **Fixed**: - `TickService`'s constructor opened with an `if (_tickObject != null) throw new InvalidOperationException("...initialized for the second time...")` guard that could never fire: `_tickObject` is a `readonly` **instance** field assigned later in that same constructor, so it was always `null` when checked. The guard advertised singleton protection the service deliberately does not provide (see `AGENTS.md` §4 — constructing multiple instances creates multiple host GameObjects, and `TickServiceTest.MultipleInstances_CreateMultipleGameObjects` pins that). Removed; `CoroutineService`, which has the same host-spawning shape, never carried one. diff --git a/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs b/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs index 6b7dd46..928f7f5 100644 --- a/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs +++ b/Editor/AddressableIds/AddressableIdsGeneratorUtils.cs @@ -516,10 +516,7 @@ private static void AppendAddressEnumMembers(StringBuilder stringBuilder, IReadO } } - // Resolves the name_filetype-suffixed disambiguation candidate for a given Addressable address when its - // cleaned name collides with one already in seenNames. Sets collided to true when the fallback path was - // taken. Used by both AppendAddressEnumMembers (to emit a unique enum member name) and - // DetectSanitizedNameCollisions (to report collisions for the Explorer diff view). + // Disambiguates a collision by re-cleaning the address with its file extension appended (name_filetype). private static string ResolveSanitizedEnumName(string address, List seenNames, out bool collided) { var name = GetCleanName(address, true); @@ -580,8 +577,7 @@ private static List DetectNullAssetTypes(IReadOnlyList SortedSetDiff(IReadOnlyList left, IReadOnlyList right) { var result = new List(); diff --git a/Editor/Explorer/Tabs/AssetResolverTab.cs b/Editor/Explorer/Tabs/AssetResolverTab.cs index b87b76b..920efa8 100644 --- a/Editor/Explorer/Tabs/AssetResolverTab.cs +++ b/Editor/Explorer/Tabs/AssetResolverTab.cs @@ -138,10 +138,7 @@ protected override void Refresh() } } - // Builds a deterministic digest of every piece of state the rebuild path renders: the not-bound branch, the - // destructive-toggle flag (gates per-row Unload buttons), and per-row (assetType, idType, id, loaded) tuples. - // When two consecutive refreshes produce the same digest the rebuild can be skipped — keeping rapid foldout - // clicks from getting destroyed mid-click by the 250 ms timer. + // Must cover every input the rebuild conditions on, the destructive toggle included; see AGENTS.md §4. private string ComputeDigest(AssetResolverService resolver) { if (resolver == null) diff --git a/Editor/Explorer/Tabs/MessageBrokerTab.cs b/Editor/Explorer/Tabs/MessageBrokerTab.cs index 347aab4..1211921 100644 --- a/Editor/Explorer/Tabs/MessageBrokerTab.cs +++ b/Editor/Explorer/Tabs/MessageBrokerTab.cs @@ -176,10 +176,7 @@ private void RefreshSubscriptionList() } } - // Builds a deterministic digest of every piece of state the rebuild path renders: edit-mode-empty, not-bound, - // and per-subscription (messageType, [target.method, ...]) tuples. When two consecutive refreshes produce the - // same digest the rebuild can be skipped — keeping rapid foldout clicks from getting destroyed mid-click by - // the 250 ms timer. + // Must cover every input the rebuild conditions on; see AGENTS.md §4. private static string ComputeDigest(bool isPlaying, MessageBrokerService broker) { if (!isPlaying) diff --git a/Editor/Explorer/Tabs/VersioningTab.cs b/Editor/Explorer/Tabs/VersioningTab.cs index 81907a7..0366748 100644 --- a/Editor/Explorer/Tabs/VersioningTab.cs +++ b/Editor/Explorer/Tabs/VersioningTab.cs @@ -195,8 +195,7 @@ private void OnRevealFile() } } - // Returns a forward-slash project-relative path (e.g. Assets/Configs/Resources) from an absolute path that - // lives under baseDir. Returns the original string unchanged if it does not start with baseDir. + // Returns fullPath unchanged when it does not live under baseDir, rather than throwing or empty. private static string GetRelativePath(string baseDir, string fullPath) { var normalBase = baseDir.Replace('\\', '/').TrimEnd('/') + '/'; diff --git a/Editor/Versioning/GitEditorProcess.cs b/Editor/Versioning/GitEditorProcess.cs index 0b66847..48490de 100644 --- a/Editor/Versioning/GitEditorProcess.cs +++ b/Editor/Versioning/GitEditorProcess.cs @@ -73,7 +73,6 @@ public void Dispose() Process?.Dispose(); } - // Execute a command eg. "status --verbose" private string ExecuteCommand(string args) { Process.StartInfo.Arguments = args; diff --git a/Editor/Versioning/VersionEditorUtils.cs b/Editor/Versioning/VersionEditorUtils.cs index 6f9f92b..381a9fb 100644 --- a/Editor/Versioning/VersionEditorUtils.cs +++ b/Editor/Versioning/VersionEditorUtils.cs @@ -48,7 +48,6 @@ public static string LoadVersionDataSerializedSync() return serialized; } - // Set the internal version for when the app plays in editor. [InitializeOnLoadMethod] private static void OnEditorLoad() { @@ -103,8 +102,6 @@ private static VersionServices.VersionData GenerateInternalVersionSuffix(bool is return data; } - // Set the internal version of this application and save it in resources. This should be called at edit/build - // time. private static void SaveVersionData(string serializedData) { var relFolderPath = VersioningEditorSettings.instance.ResourcesFolderPath; diff --git a/Editor/Versioning/VersioningMenu.cs b/Editor/Versioning/VersioningMenu.cs index 5c31ee1..e9f874c 100644 --- a/Editor/Versioning/VersioningMenu.cs +++ b/Editor/Versioning/VersioningMenu.cs @@ -10,8 +10,7 @@ namespace GameLovers.Services.Versioning.Editor /// internal static class VersioningMenu { - // Regenerates version-data.txt from the current git state (non-store build). Equivalent to the domain-reload - // trigger — useful after branch switches without a full reload. + // false = non-store build; same path the domain-reload hook takes, for use after a branch switch. [MenuItem("Tools/GameLovers/Versioning/Refresh Version Data", priority = 100)] private static void Refresh() => VersionEditorUtils.SetAndSaveInternalVersion(false); diff --git a/Runtime/RngService.cs b/Runtime/RngService.cs index 54a66fb..9e6af50 100644 --- a/Runtime/RngService.cs +++ b/Runtime/RngService.cs @@ -321,7 +321,6 @@ public static int[] GenerateRngState(int seed) return state; } - // Generates the next random number between [0...int.MaxValue] based on the given rndState private static int NextNumber(int[] rndState) { var index1 = rndState[_valueIndex] + 1; diff --git a/Runtime/VersionServices.cs b/Runtime/VersionServices.cs index a90b4a6..e349d95 100644 --- a/Runtime/VersionServices.cs +++ b/Runtime/VersionServices.cs @@ -87,13 +87,8 @@ public static string BuildNumber private static VersionData _versionData; private static bool _loaded; - // Auto-bootstrap hook: populates version metadata at the earliest runtime phase Unity exposes, before any - // scene Awake and before vendor SDK SubsystemRegistration callbacks that read VersionInternal / BuildNumber - // (e.g. Sentry's Option Config Script). Consumers no longer need to call LoadVersionData / - // LoadVersionDataAsync explicitly for the default flow. Ordering between - // RuntimeInitializeLoadType.SubsystemRegistration callbacks across assemblies is undefined; if a sibling - // SDK's hook fires before this one, the property accessors' lazy-load fallback (EnsureLoaded) covers the - // race. + // SubsystemRegistration ordering across assemblies is undefined, so a sibling SDK reading VersionInternal + // may still beat this; the accessors' EnsureLoaded fallback is what actually closes that race. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void Bootstrap() { From b0f6e2b8a7d24a2caf6d2cc6a3c85c945c9041ff Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 16:46:19 +0100 Subject: [PATCH 26/32] =?UTF-8?q?docs:=20document=20the=20remaining=20publ?= =?UTF-8?q?ic=20surface,=20per=20AGENTS.md=20=C2=A76.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes this package — Tools/style-audit.py reports 0 items for com.gamelovers.services. - Documented the internal editor-introspection accessors as one consistent set (AssetMap, ActiveAsyncCoroutines, DataEntries, Bindings, InstallerInstance, Subscriptions, IsPublishing, Pools, the three tick lists, ExtraTime, InitialTime). Each points at AGENTS.md §4 rather than restating the pattern. - Documented ObjectPoolBase's subclassing surface, keeping the two things the signatures cannot say: SpawnEntity retries through IsDestroyedOrNull because an external owner can destroy a pooled entity while it sits in the stack, and the CallOn* hooks are virtual because this base casts the entity directly whereas the GameObject pools resolve it with GetComponent. - Documented the AssetsConfigsImporter extension points and the generator importer's three abstract members; IndexOfId records that the trailing dot in "/{id}." is what stops a prefix matching. - Documented the AddressableIdsEditorSettings snapshot readouts, including that a filename or label-filter change is what makes a snapshot stale. - The 19 Explorer-tab DisplayName / RefreshIntervalMs overrides now carry `/// ` against ServiceTab's existing summaries. These were missed by the mechanical pass because it only considered overridden METHODS, not properties. Removed the `/// ` from ScriptNameEditAction.Action / Cancelled in both #if branches: they are `public override`s of a Unity base but live inside a `private` nested class, so §6.6 treats them as private. Effective accessibility, not the declared modifier, is what the rule turns on. Verified: batchmode green — EditMode 805/805, PlayMode 295/295. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 3 +++ .../AddressableIdsEditorSettings.cs | 4 +++ Editor/AssetsImporter/AssetConfigsImporter.cs | 10 +++++++ Editor/Explorer/Tabs/AddressableIdsTab.cs | 2 ++ Editor/Explorer/Tabs/AssetResolverTab.cs | 1 + Editor/Explorer/Tabs/AssetsImporterTab.cs | 2 ++ Editor/Explorer/Tabs/CoroutineTab.cs | 1 + Editor/Explorer/Tabs/DataTab.cs | 1 + Editor/Explorer/Tabs/InstallerTab.cs | 1 + Editor/Explorer/Tabs/MessageBrokerTab.cs | 1 + Editor/Explorer/Tabs/OverviewTab.cs | 2 ++ Editor/Explorer/Tabs/PoolTab.cs | 1 + Editor/Explorer/Tabs/RngTab.cs | 2 ++ Editor/Explorer/Tabs/TickTab.cs | 1 + Editor/Explorer/Tabs/TimeTab.cs | 2 ++ Editor/Explorer/Tabs/VersioningTab.cs | 2 ++ .../Windows/ServicesExplorerWindow.cs | 1 + Editor/Scaffolders/ServicesScaffolders.cs | 4 --- Runtime/AssetResolverService.cs | 5 ++++ Runtime/CommandService.cs | 2 ++ Runtime/CoroutineService.cs | 1 + Runtime/DataService.cs | 4 +++ Runtime/DependencyInjection/Installer.cs | 1 + Runtime/DependencyInjection/MainInstaller.cs | 1 + Runtime/MessageBrokerService.cs | 2 ++ Runtime/PoolService.cs | 1 + Runtime/Pooling/GameObjectPool.cs | 4 +++ Runtime/Pooling/ObjectPool.cs | 26 +++++++++++++++++++ Runtime/TickService.cs | 5 ++++ Runtime/TimeService.cs | 2 ++ Runtime/VersionServices.cs | 4 +++ 31 files changed, 95 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e003f4..64afcf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] **Docs**: +- Completed this package against the host `AGENTS.md` §6.6: `Tools/style-audit.py` now reports 0 items for `com.gamelovers.services`. Documented the internal editor-introspection accessors as a set (`AssetMap`, `ActiveAsyncCoroutines`, `DataEntries`, `Bindings`, `InstallerInstance`, `Subscriptions`, `IsPublishing`, `Pools`, the three tick lists, `ExtraTime`, `InitialTime`), each cross-referencing §4 rather than restating it; `ObjectPoolBase`'s subclassing surface, including why `SpawnEntity` loops through `IsDestroyedOrNull` and why the `CallOn*` hooks are virtual; the `AssetsConfigsImporter` extension points (`IdPattern`, `OnImportIds`, `IndexOfId`, `OnImportComplete`) and the generator importer's three abstract members; the `AddressableIdsEditorSettings` snapshot readouts, noting that a filename or label-filter change is what makes a snapshot stale; and `CommandService`'s two protected members. The 19 Explorer-tab `DisplayName` / `RefreshIntervalMs` overrides now use `/// ` against `ServiceTab`'s existing summaries. +- Removed the `/// ` from `ScriptNameEditAction.Action` / `Cancelled` (both `#if` branches). They are `public override`s of a Unity base, but sit inside a `private` nested class, so §6.6 treats them as private — a member unreachable from outside its enclosing type gets no XML doc. + - Aligned XML doc comments with the host repo's `AGENTS.md` §6.6. Removed the `AssetReferenceScene` constructor's doc block. Of the 12 doc comments on private members, the four that only restated the member's own name were deleted outright (`GitEditorProcess.ExecuteCommand`, `VersionEditorUtils.OnEditorLoad` / `SaveVersionData`, `RngService.NextNumber`); the rest were reduced to the part the code genuinely cannot state — `ObjectPoolBase.IsDestroyedOrNull`'s fake-null explanation kept in full, `VersionServices.Bootstrap` cut to the cross-assembly `SubsystemRegistration` race, `SortedSetDiff` to its unenforced pre-sorted precondition, and the two Explorer digest helpers to a pointer at this file's §4 rather than restating it. Added `/// ` to 41 overrides whose base documentation already exists (the Explorer tab `BuildUi`/`Refresh` family, plus `Equals`/`GetHashCode` on `TickData`). Moved the internal `AddressableIdsGeneratorUtils.BuildEnumSource` and `AssetResolverService.SelectAsset` test seams above their types' private blocks, per §6.6's rule that `internal` is never interleaved with `private`, and dropped the change-narration sentences from both summaries ("Extracted from …; no behaviour change versus the logic it replaces") which §6.6 forbids. **Fixed**: diff --git a/Editor/AddressableIds/AddressableIdsEditorSettings.cs b/Editor/AddressableIds/AddressableIdsEditorSettings.cs index e0c02b6..a33e5b6 100644 --- a/Editor/AddressableIds/AddressableIdsEditorSettings.cs +++ b/Editor/AddressableIds/AddressableIdsEditorSettings.cs @@ -86,9 +86,13 @@ public string AddressableLabel ? default : new DateTime(_lastGenerationUtcTicks, DateTimeKind.Utc); + /// How many ids the last generation emitted. public int LastGenerationIdCount => _lastGenerationIdCount; + /// How many labels the last generation emitted. public int LastGenerationLabelCount => _lastGenerationLabelCount; + /// Script filename the last generation used; a change from the current setting makes the snapshot stale. public string LastGenerationFilenameUsed => _lastGenerationFilenameUsed ?? string.Empty; + /// Label filter the last generation used; a change from the current setting makes the snapshot stale. public string LastGenerationLabelFilterUsed => _lastGenerationLabelFilterUsed ?? string.Empty; /// Sorted list of addressable addresses that were emitted in the last generation. Empty array when no snapshot. diff --git a/Editor/AssetsImporter/AssetConfigsImporter.cs b/Editor/AssetsImporter/AssetConfigsImporter.cs index 676b6be..631339a 100644 --- a/Editor/AssetsImporter/AssetConfigsImporter.cs +++ b/Editor/AssetsImporter/AssetConfigsImporter.cs @@ -115,11 +115,16 @@ public void Import(string assetsFolderPath = null) $"To: '{typeof(TScriptableObject).Name}' - From '{scriptableObject.AssetsFolderPath}' "); } + /// The filename fragment an id is matched against; override when assets are not named after the id. protected virtual string IdPattern(TId id) { return id.ToString(); } + /// + /// Pairs each id with the asset whose path contains it, skipping ids with no match. + /// Override to change how ids are mapped onto the discovered assets. + /// protected virtual List> OnImportIds(TScriptableObject scriptableObject, List assetGuids, List assetsPaths) @@ -142,6 +147,7 @@ protected virtual List> OnImportIds(TScriptableObject return list; } + /// Index of the first path containing /{id}., or -1; the dot is what stops a prefix matching. protected int IndexOfId(string id, IList assetsPath) { for (var i = 0; i < assetsPath.Count; i++) @@ -155,16 +161,20 @@ protected int IndexOfId(string id, IList assetsPath) return -1; } + /// Runs after the import has written the asset; the base implementation does nothing. protected virtual void OnImportComplete(TScriptableObject scriptableObject) { } } /// public abstract class AssetsConfigsGeneratorImporter : IAssetConfigsGeneratorImporter { + /// Assembly-qualified name of the id enum the generated script should use. public abstract string TIdName { get; } + /// Assembly-qualified name of the configs ScriptableObject the generated script should target. public abstract string TScriptableObjectName { get; } + /// When true the previous generated script is kept as a backup before being overwritten. public virtual bool CacheScriptAsOld => true; /// diff --git a/Editor/Explorer/Tabs/AddressableIdsTab.cs b/Editor/Explorer/Tabs/AddressableIdsTab.cs index 785fbf4..3c14fe1 100644 --- a/Editor/Explorer/Tabs/AddressableIdsTab.cs +++ b/Editor/Explorer/Tabs/AddressableIdsTab.cs @@ -23,7 +23,9 @@ public class AddressableIdsTab : ServiceTab private static readonly Color OkColor = new Color(0.6f, 0.9f, 0.6f); private static readonly Color MutedColor = new Color(0.7f, 0.7f, 0.7f); + /// public override string DisplayName => "Addressable Ids"; + /// protected override int RefreshIntervalMs => 2000; private TextField _filenameField; diff --git a/Editor/Explorer/Tabs/AssetResolverTab.cs b/Editor/Explorer/Tabs/AssetResolverTab.cs index 920efa8..e56c828 100644 --- a/Editor/Explorer/Tabs/AssetResolverTab.cs +++ b/Editor/Explorer/Tabs/AssetResolverTab.cs @@ -15,6 +15,7 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class AssetResolverTab : ServiceTab { + /// public override string DisplayName => "Asset Resolver"; private ScrollView _scroll; diff --git a/Editor/Explorer/Tabs/AssetsImporterTab.cs b/Editor/Explorer/Tabs/AssetsImporterTab.cs index 30596da..7433e8d 100644 --- a/Editor/Explorer/Tabs/AssetsImporterTab.cs +++ b/Editor/Explorer/Tabs/AssetsImporterTab.cs @@ -15,7 +15,9 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class AssetsImporterTab : ServiceTab { + /// public override string DisplayName => "Assets Importer"; + /// protected override int RefreshIntervalMs => 2000; private Toggle _autoImportToggle; diff --git a/Editor/Explorer/Tabs/CoroutineTab.cs b/Editor/Explorer/Tabs/CoroutineTab.cs index 52ee075..bfdab47 100644 --- a/Editor/Explorer/Tabs/CoroutineTab.cs +++ b/Editor/Explorer/Tabs/CoroutineTab.cs @@ -8,6 +8,7 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class CoroutineTab : ServiceTab { + /// public override string DisplayName => "Coroutine"; private ScrollView _scroll; diff --git a/Editor/Explorer/Tabs/DataTab.cs b/Editor/Explorer/Tabs/DataTab.cs index a8cd274..17311bd 100644 --- a/Editor/Explorer/Tabs/DataTab.cs +++ b/Editor/Explorer/Tabs/DataTab.cs @@ -13,6 +13,7 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class DataTab : ServiceTab { + /// public override string DisplayName => "Data"; private ScrollView _scroll; diff --git a/Editor/Explorer/Tabs/InstallerTab.cs b/Editor/Explorer/Tabs/InstallerTab.cs index 6e86b6a..e6f5b9a 100644 --- a/Editor/Explorer/Tabs/InstallerTab.cs +++ b/Editor/Explorer/Tabs/InstallerTab.cs @@ -11,6 +11,7 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class InstallerTab : ServiceTab { + /// public override string DisplayName => "Installer"; private ScrollView _scroll; diff --git a/Editor/Explorer/Tabs/MessageBrokerTab.cs b/Editor/Explorer/Tabs/MessageBrokerTab.cs index 1211921..a6beb38 100644 --- a/Editor/Explorer/Tabs/MessageBrokerTab.cs +++ b/Editor/Explorer/Tabs/MessageBrokerTab.cs @@ -15,6 +15,7 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class MessageBrokerTab : ServiceTab { + /// public override string DisplayName => "Message Broker"; private ScrollView _scroll; diff --git a/Editor/Explorer/Tabs/OverviewTab.cs b/Editor/Explorer/Tabs/OverviewTab.cs index 994ee40..18b77d7 100644 --- a/Editor/Explorer/Tabs/OverviewTab.cs +++ b/Editor/Explorer/Tabs/OverviewTab.cs @@ -16,7 +16,9 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class OverviewTab : ServiceTab { + /// public override string DisplayName => "Overview"; + /// protected override int RefreshIntervalMs => 1000; private readonly ServicesExplorerWindow _window; diff --git a/Editor/Explorer/Tabs/PoolTab.cs b/Editor/Explorer/Tabs/PoolTab.cs index 880d98c..8b18c2b 100644 --- a/Editor/Explorer/Tabs/PoolTab.cs +++ b/Editor/Explorer/Tabs/PoolTab.cs @@ -12,6 +12,7 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class PoolTab : ServiceTab { + /// public override string DisplayName => "Pool"; private ScrollView _scroll; diff --git a/Editor/Explorer/Tabs/RngTab.cs b/Editor/Explorer/Tabs/RngTab.cs index fb3b9af..9de7b5c 100644 --- a/Editor/Explorer/Tabs/RngTab.cs +++ b/Editor/Explorer/Tabs/RngTab.cs @@ -10,7 +10,9 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class RngTab : ServiceTab { + /// public override string DisplayName => "RNG"; + /// protected override int RefreshIntervalMs => 500; private const string PeekTooltip = diff --git a/Editor/Explorer/Tabs/TickTab.cs b/Editor/Explorer/Tabs/TickTab.cs index 939c559..fbe4c36 100644 --- a/Editor/Explorer/Tabs/TickTab.cs +++ b/Editor/Explorer/Tabs/TickTab.cs @@ -8,6 +8,7 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class TickTab : ServiceTab { + /// public override string DisplayName => "Tick"; private Foldout _updateFoldout; diff --git a/Editor/Explorer/Tabs/TimeTab.cs b/Editor/Explorer/Tabs/TimeTab.cs index f31c35e..c2c38a6 100644 --- a/Editor/Explorer/Tabs/TimeTab.cs +++ b/Editor/Explorer/Tabs/TimeTab.cs @@ -10,7 +10,9 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class TimeTab : ServiceTab { + /// public override string DisplayName => "Time"; + /// protected override int RefreshIntervalMs => 500; private Label _utcLabel; diff --git a/Editor/Explorer/Tabs/VersioningTab.cs b/Editor/Explorer/Tabs/VersioningTab.cs index 0366748..1096c47 100644 --- a/Editor/Explorer/Tabs/VersioningTab.cs +++ b/Editor/Explorer/Tabs/VersioningTab.cs @@ -14,7 +14,9 @@ namespace GameLovers.Services.Editor.Explorer.Tabs /// public class VersioningTab : ServiceTab { + /// public override string DisplayName => "Versioning"; + /// protected override int RefreshIntervalMs => 2000; private Label _externalLabel; diff --git a/Editor/Explorer/Windows/ServicesExplorerWindow.cs b/Editor/Explorer/Windows/ServicesExplorerWindow.cs index c3c7725..c60d5bd 100644 --- a/Editor/Explorer/Windows/ServicesExplorerWindow.cs +++ b/Editor/Explorer/Windows/ServicesExplorerWindow.cs @@ -19,6 +19,7 @@ public class ServicesExplorerWindow : EditorWindow private TabView _tabView; private readonly List _tabs = new List(); + /// Opens the Services Explorer window. [MenuItem("Tools/GameLovers/Services Explorer")] public static void Open() { diff --git a/Editor/Scaffolders/ServicesScaffolders.cs b/Editor/Scaffolders/ServicesScaffolders.cs index 3adae0b..6118a1b 100644 --- a/Editor/Scaffolders/ServicesScaffolders.cs +++ b/Editor/Scaffolders/ServicesScaffolders.cs @@ -106,13 +106,11 @@ private class ScriptNameEditAction : AssetCreationEndAction { public string TemplatePath; - /// public override void Action(EntityId entityId, string pathName, string resourceFile) { WriteScript(pathName); } - /// public override void Cancelled(EntityId entityId, string pathName, string resourceFile) { } @@ -181,13 +179,11 @@ private class ScriptNameEditAction : EndNameEditAction { public string TemplatePath; - /// public override void Action(int instanceId, string pathName, string resourceFile) { WriteScript(pathName); } - /// public override void Cancelled(int instanceId, string pathName, string resourceFile) { } diff --git a/Runtime/AssetResolverService.cs b/Runtime/AssetResolverService.cs index a708c01..5d38515 100644 --- a/Runtime/AssetResolverService.cs +++ b/Runtime/AssetResolverService.cs @@ -128,6 +128,7 @@ public class AssetResolverService : AddressablesAssetLoader, IAssetAdderService private readonly IDictionary> _assetMap = new Dictionary>(); + /// Registered assets, keyed by asset type then id type. Editor introspection only — see AGENTS.md §4. internal IReadOnlyDictionary> AssetMap => (IReadOnlyDictionary>)_assetMap; @@ -413,6 +414,10 @@ public void AddDebugConfigs(Sprite errorSprite = null, GameObject errorCube = nu _errorClip = errorClip; } + /// + /// Resolves which asset to hand back, substituting the matching error placeholder when the + /// reference has not finished loading. + /// internal static TAsset SelectAsset(Type type, Object asset, bool isDone, bool instantiate, Sprite errorSprite, GameObject errorCube, Material errorMaterial, AudioClip errorClip) where TAsset : Object diff --git a/Runtime/CommandService.cs b/Runtime/CommandService.cs index e143dd9..eea41ec 100644 --- a/Runtime/CommandService.cs +++ b/Runtime/CommandService.cs @@ -10,7 +10,9 @@ public class CommandService : ICommandService where TGam private readonly TGameLogic _gameLogic; private readonly IMessageBrokerService _messageBroker; + /// The game logic every command executes against. protected TGameLogic GameLogic => _gameLogic; + /// The broker commands publish through. protected IMessageBrokerService MessageBroker => _messageBroker; public CommandService(TGameLogic gameLogic, IMessageBrokerService messageBroker) diff --git a/Runtime/CoroutineService.cs b/Runtime/CoroutineService.cs index 848d0eb..024411b 100644 --- a/Runtime/CoroutineService.cs +++ b/Runtime/CoroutineService.cs @@ -111,6 +111,7 @@ public class CoroutineService : ICoroutineService #if UNITY_EDITOR private readonly List _activeAsyncCoroutines = new List(); + /// Async coroutines still running. Editor introspection only — see AGENTS.md §4. internal IReadOnlyList ActiveAsyncCoroutines => _activeAsyncCoroutines; #endif diff --git a/Runtime/DataService.cs b/Runtime/DataService.cs index 9505a49..e35c358 100644 --- a/Runtime/DataService.cs +++ b/Runtime/DataService.cs @@ -55,6 +55,7 @@ public class DataService : IDataService { private readonly IDictionary _data = new Dictionary(); + /// The in-memory store, keyed by data type. Editor introspection only — see AGENTS.md §4. internal IReadOnlyDictionary DataEntries => (IReadOnlyDictionary)_data; /// @@ -115,6 +116,9 @@ public void AddOrReplaceData(T data) where T : class } } + /// + /// Runs after a save has been written; override to mirror the data elsewhere. + /// protected virtual void OnDataSaved(string key, object data, Type type) { } diff --git a/Runtime/DependencyInjection/Installer.cs b/Runtime/DependencyInjection/Installer.cs index ea5c0ad..201f3cd 100644 --- a/Runtime/DependencyInjection/Installer.cs +++ b/Runtime/DependencyInjection/Installer.cs @@ -89,6 +89,7 @@ public class Installer : IInstaller { private readonly Dictionary _bindings = new Dictionary(); + /// Current interface-to-instance bindings. Editor introspection only — see AGENTS.md §4. internal IReadOnlyDictionary Bindings => _bindings; /// diff --git a/Runtime/DependencyInjection/MainInstaller.cs b/Runtime/DependencyInjection/MainInstaller.cs index 6a60a90..4ea5c4a 100644 --- a/Runtime/DependencyInjection/MainInstaller.cs +++ b/Runtime/DependencyInjection/MainInstaller.cs @@ -12,6 +12,7 @@ public static class MainInstaller { private static readonly Installer _installer = new Installer(); + /// The single installer this static facade wraps. Editor introspection only — see AGENTS.md §4. internal static Installer InstallerInstance => _installer; /// diff --git a/Runtime/MessageBrokerService.cs b/Runtime/MessageBrokerService.cs index e78ae42..042a60e 100644 --- a/Runtime/MessageBrokerService.cs +++ b/Runtime/MessageBrokerService.cs @@ -66,8 +66,10 @@ public class MessageBrokerService : IMessageBrokerService private (bool, IMessage) _isPublishing; + /// Subscribers keyed by message type, then by subscriber target. Editor introspection only — see AGENTS.md §4. internal IReadOnlyDictionary> Subscriptions => (IReadOnlyDictionary>)_subscriptions; + /// True while a publish is in flight, when mutating the subscriber list would throw. Editor introspection only — see AGENTS.md §4. internal bool IsPublishing => _isPublishing.Item1; /// diff --git a/Runtime/PoolService.cs b/Runtime/PoolService.cs index e670181..8227008 100644 --- a/Runtime/PoolService.cs +++ b/Runtime/PoolService.cs @@ -11,6 +11,7 @@ public class PoolService : IPoolService { private readonly IDictionary _pools = new Dictionary(); + /// Registered pools, one per pooled type. Editor introspection only — see AGENTS.md §4. internal IReadOnlyDictionary Pools => (IReadOnlyDictionary)_pools; /// diff --git a/Runtime/Pooling/GameObjectPool.cs b/Runtime/Pooling/GameObjectPool.cs index 2b573bb..d298689 100644 --- a/Runtime/Pooling/GameObjectPool.cs +++ b/Runtime/Pooling/GameObjectPool.cs @@ -66,6 +66,7 @@ public static GameObject Instantiator(GameObject entityRef) return instance; } + /// protected override GameObject SpawnEntity() { var entity = base.SpawnEntity(); @@ -99,6 +100,7 @@ protected override void CallOnDespawned(GameObject entity) poolEntity?.OnDespawn(); } + /// protected override void PostDespawnEntity(GameObject entity) { entity.SetActive(false); @@ -173,6 +175,7 @@ public static T Instantiator(T entityRef) return instance; } + /// protected override T SpawnEntity() { T entity = null; @@ -218,6 +221,7 @@ protected override void CallOnDespawned(T entity) poolEntity?.OnDespawn(); } + /// protected override void PostDespawnEntity(T entity) { entity.gameObject.SetActive(false); diff --git a/Runtime/Pooling/ObjectPool.cs b/Runtime/Pooling/ObjectPool.cs index 0b3d7f3..18316b5 100644 --- a/Runtime/Pooling/ObjectPool.cs +++ b/Runtime/Pooling/ObjectPool.cs @@ -81,6 +81,10 @@ public void DespawnAll() } } + /// + /// Clears the pool, additionally dropping the reference to + /// when is set. + /// public virtual void Dispose(bool disposeSampleEntity) { if (disposeSampleEntity) @@ -163,6 +167,11 @@ public virtual void Dispose() Clear(); } + /// + /// Takes the next entity from the stack, instantiating one when the stack is empty. + /// Retries past entities an external owner destroyed while they sat pooled, which is why the + /// null test goes through IsDestroyedOrNull rather than a plain == null. + /// protected virtual T SpawnEntity() { T entity = null; @@ -180,8 +189,15 @@ protected virtual T SpawnEntity() return entity; } + /// + /// Runs after an entity has been returned to the stack; the base implementation does nothing. + /// protected virtual void PostDespawnEntity(T entity) { } + /// + /// Instantiates a fresh entity from the sample and, when it implements + /// , hands it back a reference to this pool. + /// protected T CallInstantiator() { var entity = _instantiator.Invoke(SampleEntity); @@ -192,6 +208,10 @@ protected T CallInstantiator() return entity; } + /// + /// Dispatches the spawn hook. Override to change how a pooled entity is discovered — + /// this base casts the entity directly, whereas the GameObject pools use GetComponent. + /// protected virtual void CallOnSpawned(T entity) { var poolEntity = entity as IPoolEntitySpawn; @@ -199,6 +219,9 @@ protected virtual void CallOnSpawned(T entity) poolEntity?.OnSpawn(); } + /// + /// Dispatches the data-carrying spawn hook; see the parameterless overload for the cast rationale. + /// protected virtual void CallOnSpawned(T entity, TData data) { var poolEntity = entity as IPoolEntitySpawn; @@ -206,6 +229,9 @@ protected virtual void CallOnSpawned(T entity, TData data) poolEntity?.OnSpawn(data); } + /// + /// Dispatches the despawn hook; see for the cast rationale. + /// protected virtual void CallOnDespawned(T entity) { var poolEntity = entity as IPoolEntityDespawn; diff --git a/Runtime/TickService.cs b/Runtime/TickService.cs index 661886b..30667b2 100644 --- a/Runtime/TickService.cs +++ b/Runtime/TickService.cs @@ -114,8 +114,11 @@ public class TickService : ITickService private int _tickDataIdRef; + /// Subscribers driven from Update. Editor introspection only — see AGENTS.md §4. internal IReadOnlyList OnUpdateList => _onUpdateList; + /// Subscribers driven from FixedUpdate. Editor introspection only — see AGENTS.md §4. internal IReadOnlyList OnFixedUpdateList => _onFixedUpdateList; + /// Subscribers driven from LateUpdate. Editor introspection only — see AGENTS.md §4. internal IReadOnlyList OnLateUpdateList => _onLateUpdateList; public TickService() @@ -361,6 +364,7 @@ private void Update(List list) } } + /// One tick subscription: its action, cadence and bookkeeping. Editor introspection only — see AGENTS.md §4. internal struct TickData { public int Id; @@ -371,6 +375,7 @@ internal struct TickData public float LastTickTime; public object Subscriber; + /// Two entries match on alone, which is what unsubscribe relies on. public bool Equals(TickData other) { return other.Id == Id; diff --git a/Runtime/TimeService.cs b/Runtime/TimeService.cs index 0aa2180..bd9acfe 100644 --- a/Runtime/TimeService.cs +++ b/Runtime/TimeService.cs @@ -79,7 +79,9 @@ public class TimeService : ITimeManipulator private float _extraTime; private DateTime _initialTime = DateTime.MinValue; + /// Seconds added by AddTime, on top of real elapsed time. Editor introspection only — see AGENTS.md §4. internal float ExtraTime => _extraTime; + /// The reference instant every conversion is measured from. Editor introspection only — see AGENTS.md §4. internal DateTime InitialTime => _initialTime; /// diff --git a/Runtime/VersionServices.cs b/Runtime/VersionServices.cs index e349d95..f0cd896 100644 --- a/Runtime/VersionServices.cs +++ b/Runtime/VersionServices.cs @@ -16,6 +16,10 @@ public static class VersionServices { public const string VersionDataFilename = "version-data"; + /// + /// The shape of the version-data Resources text asset written by the editor's + /// versioning pass and parsed on load. + /// [Serializable] public struct VersionData { From 1b06b415eff08b798a418ec79a2add28feca6d60 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 17:57:53 +0100 Subject: [PATCH 27/32] docs: refine Services 2.1.2 release changelog --- CHANGELOG.md | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64afcf1..8907166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,32 +4,16 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased] - -**Docs**: -- Completed this package against the host `AGENTS.md` §6.6: `Tools/style-audit.py` now reports 0 items for `com.gamelovers.services`. Documented the internal editor-introspection accessors as a set (`AssetMap`, `ActiveAsyncCoroutines`, `DataEntries`, `Bindings`, `InstallerInstance`, `Subscriptions`, `IsPublishing`, `Pools`, the three tick lists, `ExtraTime`, `InitialTime`), each cross-referencing §4 rather than restating it; `ObjectPoolBase`'s subclassing surface, including why `SpawnEntity` loops through `IsDestroyedOrNull` and why the `CallOn*` hooks are virtual; the `AssetsConfigsImporter` extension points (`IdPattern`, `OnImportIds`, `IndexOfId`, `OnImportComplete`) and the generator importer's three abstract members; the `AddressableIdsEditorSettings` snapshot readouts, noting that a filename or label-filter change is what makes a snapshot stale; and `CommandService`'s two protected members. The 19 Explorer-tab `DisplayName` / `RefreshIntervalMs` overrides now use `/// ` against `ServiceTab`'s existing summaries. -- Removed the `/// ` from `ScriptNameEditAction.Action` / `Cancelled` (both `#if` branches). They are `public override`s of a Unity base, but sit inside a `private` nested class, so §6.6 treats them as private — a member unreachable from outside its enclosing type gets no XML doc. - -- Aligned XML doc comments with the host repo's `AGENTS.md` §6.6. Removed the `AssetReferenceScene` constructor's doc block. Of the 12 doc comments on private members, the four that only restated the member's own name were deleted outright (`GitEditorProcess.ExecuteCommand`, `VersionEditorUtils.OnEditorLoad` / `SaveVersionData`, `RngService.NextNumber`); the rest were reduced to the part the code genuinely cannot state — `ObjectPoolBase.IsDestroyedOrNull`'s fake-null explanation kept in full, `VersionServices.Bootstrap` cut to the cross-assembly `SubsystemRegistration` race, `SortedSetDiff` to its unenforced pre-sorted precondition, and the two Explorer digest helpers to a pointer at this file's §4 rather than restating it. Added `/// ` to 41 overrides whose base documentation already exists (the Explorer tab `BuildUi`/`Refresh` family, plus `Equals`/`GetHashCode` on `TickData`). Moved the internal `AddressableIdsGeneratorUtils.BuildEnumSource` and `AssetResolverService.SelectAsset` test seams above their types' private blocks, per §6.6's rule that `internal` is never interleaved with `private`, and dropped the change-narration sentences from both summaries ("Extracted from …; no behaviour change versus the logic it replaces") which §6.6 forbids. - -**Fixed**: -- `TickService`'s constructor opened with an `if (_tickObject != null) throw new InvalidOperationException("...initialized for the second time...")` guard that could never fire: `_tickObject` is a `readonly` **instance** field assigned later in that same constructor, so it was always `null` when checked. The guard advertised singleton protection the service deliberately does not provide (see `AGENTS.md` §4 — constructing multiple instances creates multiple host GameObjects, and `TickServiceTest.MultipleInstances_CreateMultipleGameObjects` pins that). Removed; `CoroutineService`, which has the same host-spawning shape, never carried one. -- `GameObjectPool.Dispose(bool disposeSampleEntity)` and `GameObjectPool.Dispose(bool)` destroyed `SampleEntity` unconditionally, ignoring the argument — `Dispose(false)` destroyed a sample entity the caller explicitly asked to keep. -- `ObjectPoolBase.SpawnEntity` could hand out a destroyed pooled object. Its retry loop tested `entity == null`, which inside a generic constrained only to `class` compiles to plain reference equality and never reaches `UnityEngine.Object`'s overloaded `==` that detects a destroyed ("fake-null") native object. A pooled `GameObject`/`Behaviour` destroyed by an external owner while despawned was therefore returned to the next `Spawn()` caller, who hit a `MissingReferenceException` a frame later. Now routed through a runtime type check that dispatches to the Unity overload when the entity is a `UnityEngine.Object`, and falls back to reference equality for POCO pooled types. -- `AddressableIdsGeneratorUtils` emitted duplicate enum members when two Addressable addresses sanitized to the same C# identifier (e.g. `ui/main-menu` and `ui-main/menu` both clean to `ui_main_menu`), producing generated code that does not compile. The member-append path now uses the disambiguated name it already computed instead of re-deriving the raw cleaned name. +## [2.1.2] - 2026-08-04 **Changed**: -- `package.json` now declares `com.unity.test-framework.performance` (3.5.0). Both test asmdefs already referenced `Unity.PerformanceTesting` unconditionally, so consumers without that package installed hit a missing-assembly compile error in this package's test assemblies. - -## [2.1.2] - 2026-07-29 +- Added the `com.unity.test-framework.performance` (3.5.0) dependency so the package's test assemblies compile when tests are enabled. **Fixed**: -- Renamed `Tests/EditMode/GameLovers.Services.Tests.asmdef` to `GameLovers.Services.Editor.Tests.asmdef` to match its own `name` field (`GameLovers.Services.Editor.Tests`); GUID preserved via `git mv` on the paired `.meta`. -- `Tests/PlayMode/GameLovers.Services.Tests.Playmode.asmdef` no longer sets `autoReferenced: true` (was the only test asmdef in the repo doing so). Test discovery is unaffected — the Test Runner finds test assemblies via the `UNITY_INCLUDE_TESTS` define constraint, independent of `autoReferenced`. - -**Docs**: -- `Samples~/ServicesPlayground`'s `package.json` description no longer claims the sample UI "is built programmatically" — it ships as a hand-authored prefab (`ServicesPlaygroundUI.prefab`) with a `[SerializeField]`-wired driver script. -- Converged `README.md`'s repository links on the actual origin. +- Fixed `GameObjectPool.Dispose(false)` and its generic equivalent so they preserve the sample entity when requested. +- Fixed pooling of Unity objects so destroyed pooled `GameObject` and `Behaviour` instances are skipped instead of being returned to callers. +- Fixed Addressable ID enum generation so addresses that sanitize to the same C# identifier receive distinct members. +- Fixed the Services Playground Input System setup by assigning default actions when its UI module is created, so sample controls respond as expected. ## [2.1.1] - 2026-07-04 From 2a38ec4e46c813e0c6826a70487d31b2262e014d Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 23:00:59 +0100 Subject: [PATCH 28/32] ci: add release-preflight check for develop->master PRs --- .github/workflows/release-preflight.yml | 69 +++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/release-preflight.yml diff --git a/.github/workflows/release-preflight.yml b/.github/workflows/release-preflight.yml new file mode 100644 index 0000000..9c7d2dd --- /dev/null +++ b/.github/workflows/release-preflight.yml @@ -0,0 +1,69 @@ +# Release preflight — required status check for develop -> master PRs. +# +# Installed into each package repo by: +# release.py install-preflight +# +# WHY THIS EXISTS +# These are solo repos: GitHub refuses to let a PR author review their own PR +# (HTTP 422 "Review cannot be requested from pull request author"), so a human +# review gate is unavailable. This gives a real blocking gate instead — the same +# package-local gates the release driver enforces, run before the merge rather +# than after it. +# +# WHY IT IS SAFE TO SHIP IN A PACKAGE REPO +# Verified against upm 9.31.1 with a positive control: `.github/` is excluded +# from the packed tarball automatically, even when it is not gitignored, while a +# non-ignored control file at the package root IS packed. (Older packers did not +# exclude it — the published googlesheetimporter 0.7.2 asset still contains +# `package/.github/workflows/openai.yml`.) So no `.npmignore` is required. If +# that ever regresses, `G24` will surface it as an unexpected added file. +# +# WHAT IT DOES NOT CHECK +# Only gates decidable from the package directory plus the base ref, so the check +# needs no token, no submodules and no network: G7 (bare SemVer), G8/G9 (CHANGELOG +# heading matches package.json and is newest+highest), G10 (date sane), G11 +# (version advances past master), G15 (the PR touches both files). Remote-state +# gates (G0-G6, G12-G14) and the whole tarball chain (G20-G27) run locally in +# `release.py preflight` / `pack` before the PR is opened. + +name: release-preflight + +on: + pull_request: + branches: [master] + +permissions: + contents: read + +concurrency: + group: release-preflight-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + preflight: + runs-on: ubuntu-latest + steps: + - name: Check out the package + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Check out the release tooling + uses: actions/checkout@v4 + with: + repository: CoderGamester/Frameworks + path: .release-tooling + sparse-checkout: .claude/skills/unity-package-release/scripts + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Preflight + run: | + python3 .release-tooling/.claude/skills/unity-package-release/scripts/release.py \ + preflight-pr --path . --base "origin/${{ github.base_ref }}" From 73dffec91c4be54f8050af5f076ccd3240a976ef Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 23:03:36 +0100 Subject: [PATCH 29/32] ci: add release-preflight check for develop->master PRs --- .github/workflows/release-preflight.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-preflight.yml b/.github/workflows/release-preflight.yml index 9c7d2dd..1300736 100644 --- a/.github/workflows/release-preflight.yml +++ b/.github/workflows/release-preflight.yml @@ -49,13 +49,18 @@ jobs: fetch-depth: 0 persist-credentials: false + # The gate logic is shared rather than vendored into six repos, so there is + # one source of truth. `ref` is explicit: actions/checkout defaults to the + # target repo's DEFAULT branch (master), where the tooling does not exist + # yet — omitting it fails with "No such file or directory". Retarget this to + # master once the skill is merged there. - name: Check out the release tooling uses: actions/checkout@v4 with: repository: CoderGamester/Frameworks + ref: develop path: .release-tooling sparse-checkout: .claude/skills/unity-package-release/scripts - sparse-checkout-cone-mode: false persist-credentials: false - name: Set up Python From a16f0b6b5e090561fc0b24424fd4b28b8e9836e1 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 23:11:56 +0100 Subject: [PATCH 30/32] fix: restore CRLF line endings in CHANGELOG.md A docs pass rewrote the file with LF, changing every historical byte and breaking the release-notes validator's baseline comparison. Restores the committed convention per AGENTS.md; no content change. --- CHANGELOG.md | 626 +++++++++++++++++++++++++-------------------------- 1 file changed, 313 insertions(+), 313 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8907166..11b772b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,313 +1,313 @@ -# Changelog -All notable changes to this package will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - -## [2.1.2] - 2026-08-04 - -**Changed**: -- Added the `com.unity.test-framework.performance` (3.5.0) dependency so the package's test assemblies compile when tests are enabled. - -**Fixed**: -- Fixed `GameObjectPool.Dispose(false)` and its generic equivalent so they preserve the sample entity when requested. -- Fixed pooling of Unity objects so destroyed pooled `GameObject` and `Behaviour` instances are skipped instead of being returned to callers. -- Fixed Addressable ID enum generation so addresses that sanitize to the same C# identifier receive distinct members. -- Fixed the Services Playground Input System setup by assigning default actions when its UI module is created, so sample controls respond as expected. - -## [2.1.1] - 2026-07-04 - -**Changed**: -- PlayMode tests updated to parameterless `FindObjectsByType()` overload (Unity 6 API). -- Removed redundant `[Serializable]` from `AddressableConfig` (already implicitly serializable as a reference type in Unity YAML). - -**Fixed**: -- `ServicesScaffolders` adapts to Unity 6000.4+ `AssetCreationEndAction` / `EntityId` API (guarded by `UNITY_6000_4_OR_NEWER`; pre-6000.4 path unchanged). - -## [2.1.0] - 2026-05-20 - -**New**: -- `VersionServices` now auto-bootstraps via `[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]`, populating version metadata before any scene `Awake` callback and before vendor-SDK `SubsystemRegistration` callbacks that read it. Consumers no longer need to call `LoadVersionData()` / `LoadVersionDataAsync()` explicitly for the default flow. - -**Changed**: -- Property getters (`VersionInternal`, `Branch`, `Commit`, `BuildNumber`) now lazy-load via a new private `EnsureLoaded()` on first access if the auto-bootstrap hook has not yet fired — protects against undefined ordering between sibling assemblies' `[RuntimeInitializeOnLoadMethod]` callbacks at the same phase. -- Removed the private `IsLoaded()` helper (replaced by `EnsureLoaded()` invoked from each property getter). - -**Docs**: -- `docs/version-services.md` rewritten around the new auto-bootstrap contract: the recommended usage no longer includes any explicit load call, the lazy-load fallback is documented, and the Error Reference table now describes the fallback behaviour (no exception is raised). - -## [2.0.2] - 2026-05-20 - -**Fixed**: -- Add missing meta file - -## [2.0.1] - 2026-05-20 - -**New** -- Added new test suite for more rebust code coverage -- Added `VersionServices.LoadVersionData()` — synchronous sibling of `LoadVersionDataAsync()` for consumers who want to populate version metadata at boot without an `await`. Both methods now funnel into a shared private `ApplyTextAsset` helper, so behaviour is identical. Sync is the recommended default for the shipping `version-data.txt` (a few hundred bytes); async remains available for cases where `VersionData` is extended with large embedded blobs. Covered by `Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs`. - -**Fixed**: -- `Tests/AGENTS.md` §8 extended with a new **"Authorized reflection sites (storage-assertion exception)"** subsection. - -## [2.0.0] - 2026-04-26 - -**New**: -- Added **Services Explorer** window (`Tools > GameLovers > Services Explorer`) with 13 live-refresh tabs: Overview, Installer, MessageBroker, Tick, Coroutine, Pool, Data, Time, RNG, AssetResolver, Versioning, Assets Importer, Addressable Ids — works in both Edit and Play mode -- Menu stubs under `Tools > GameLovers`: - - `Versioning / Refresh Version Data` and `Versioning / Open in Explorer` - - `Assets Importer / Import Assets Data` and `Assets Importer / Open in Explorer` - - `Addressable Ids / Generate Addressable Ids` and `Addressable Ids / Open in Explorer` -- Added `Assets > Create > GameLovers Services > …` scaffolders: Message, Command, Service, Pool Entity (template-based, $NAME$ / $NAMESPACE$ substitution) -- Absorbed `com.gamelovers.assetsimporter` v0.5.2 into this package -- Added `IAssetLoader`, `ISceneLoader`, `AddressablesAssetLoader` to `Runtime/AssetsImporter/` (ns `GameLovers.Services.AssetsImporter`) -- Added `AddressableConfig`, `AssetConfigsScriptableObject`, `AssetLoaderUtils`, `AssetReferenceScene` to `Runtime/AssetsImporter/` (ns `GameLovers.Services.AssetsImporter`) -- Added `AssetResolverService` (implements `IAssetResolverService` / `IAssetAdderService`) to `Runtime/` root (ns `GameLovers.Services`) -- Added Editor/AssetsImporter/: `AssetsImporter`, `AssetsToolImporter`, `AssetConfigsImporter`, `AddressableIdsGenerator`, `AddressablesIdGeneratorSettings` (ns `GameLovers.Services.AssetsImporter.Editor`) -- Added importable **Samples** under `Samples~/` (importable via Unity Package Manager > GameLovers Services > Samples). - - **Services Playground** — single-scene, zero-setup walk-through that wires every foundation service via `MainInstaller` and exercises 10 of 13 Services Explorer tabs end-to-end. - - **Asset Resolver** — focused demo of `AssetResolverService` end-to-end (`AddConfigs` / `RequestAsset` / `UnloadAssets`) with `SpriteConfigs : AssetConfigsScriptableObject`. Drives the three Services Explorer tabs the Playground does not cover (Asset Resolver, Assets Importer, Addressable Ids). - -**Changed**: -- Addressable Ids generator and Assets Importer settings moved from `Assets/*.asset` ScriptableObjects to `ProjectSettings/` ScriptableSingletons (mirrors `VersioningEditorSettings`). -- Generation logic extracted from `AddressableIdsGenerator` into `AddressableIdsGeneratorUtils` (static `internal`); importer discovery/import logic extracted into `AssetsImporterEditorUtils`. -- `Tools/Assets Importer/*` and `Tools/AddressableIds Generator/*` menu entries removed. Use `Tools/GameLovers/Assets Importer/...`, `Tools/GameLovers/Addressable Ids/...`, or the Services Explorer tabs instead. -- `Toggle Auto Import On Refresh` menu entry removed. The toggle now lives exclusively in the Services Explorer **Assets Importer** tab. -- `Assets/AssetsImporter.asset` and `Assets/AddressablesIdGeneratorSettings.asset` are no longer used (settings moved to `ProjectSettings/`); safe to delete from consumer projects. -- Deleted editor source files: `AssetsImporter.cs`, `AssetsToolImporter.cs`, `AddressableIdsGenerator.cs`, `AddressablesIdGeneratorSettings.cs`. -- Folder reorganization: `Runtime/` now has domain subfolders `DependencyInjection/`, `Commands/`, `Pooling/`, `AssetsImporter/`; `Editor/` now has `Versioning/` and `AssetsImporter/` subfolders -- `Installer.cs` and `MainInstaller.cs` moved to `Runtime/DependencyInjection/` (namespace unchanged: `GameLovers.Services`) -- `CommandService.cs` trimmed to concrete class only; command contract interfaces extracted to `Runtime/Commands/` under ns `GameLovers.Services.Commands` -- `PoolService.cs` trimmed to concrete class only; pool interfaces + implementations moved to `Runtime/Pooling/` under ns `GameLovers.Services.Pooling` -- `ObjectPool.cs` (578 lines, 10 types) split into 4 files under `Runtime/Pooling/`: `IPoolEntity.cs`, `IObjectPool.cs`, `ObjectPool.cs`, `GameObjectPool.cs` -- `VersionEditorUtils.cs` and `GitEditorProcess.cs` moved to `Editor/Versioning/`; re-namespaced from `GameLovers.Services.Editor` → `GameLovers.Services.Versioning.Editor` -- Added new hard dependencies: `com.unity.addressables` (1.21.20) and `com.cysharp.unitask` (2.5.10) - -**Fixed**: -- `AddressablesAssetLoader.UnloadAsset` no longer calls `GC.Collect()`, `GC.WaitForPendingFinalizers()`, or `Resources.UnloadUnusedAssets()`. The method now only decrements the Addressables reference count. The old implementation caused PlayMode Test Runner crashes on macOS and O(total-assets-in-memory) main-thread stalls per per-asset release. Callers that need memory reclamation should invoke `Resources.UnloadUnusedAssets()` themselves at appropriate moments (scene transitions, boot, memory-pressure events); Unity also runs an unused-assets sweep automatically on `LoadSceneMode.Single` scene loads. -- Corrected `IAssetLoader.UnloadAsset` XML documentation: removed the incorrect "will also destroy GameObject instances" claim — `Addressables.Release(gameObject)` does not destroy the instance; callers must `Object.Destroy` it separately. -- `IAsyncCoroutine.StopCoroutine(bool triggerOnComplete)` now honors its `triggerOnComplete` parameter and flips `IsCompleted` to `true` and `IsRunning` to `false` after stopping. The previous implementation always invoked `OnComplete` callbacks regardless of the flag and left state flags unchanged, so consumers could not distinguish a stopped coroutine from a running one and `triggerOnComplete: false` was silently ignored. -- `GameObjectPool.Dispose()` and `GameObjectPool.Dispose()` now skip pooled entries whose underlying `GameObject` has already been destroyed by an external owner (e.g. a parent GameObject was destroyed while pooled instances were still reparented under it via `DespawnToSampleParent`). - -**Breaking Changes** — see `MIGRATION.md` for details: -- Pool types moved from `GameLovers.Services` to `GameLovers.Services.Pooling` (`IPoolService`, `IObjectPool`, `IObjectPool`, `ObjectPool`, `ObjectPoolBase`, `GameObjectPool`, `GameObjectPool`, `IPoolEntitySpawn`, `IPoolEntitySpawn`, `IPoolEntityDespawn`, `IPoolEntityObject`). `PoolService` concrete class remains in `GameLovers.Services`. -- Command contract types moved from `GameLovers.Services` to `GameLovers.Services.Commands` (`IGameCommandBase`, `IGameCommand<>`, `IGameServerCommand<>`, `ICommandService<>`). `CommandService<>` concrete class remains in `GameLovers.Services`. -- `GameLovers.AssetsImporter.*` renamed to `GameLovers.Services.AssetsImporter.*` -- `GameLovers.AssetsImporter.AssetResolverService` is now `GameLovers.Services.AssetResolverService` -- `GameLovers.Services.Editor.*` (versioning editor) renamed to `GameLovers.Services.Versioning.Editor.*` -- `IAssetLoader.UnloadAssetAsync(T, Action)` → `IAssetLoader.UnloadAsset(T, Action)`: method renamed (dropped `Async` suffix) and return type changed from `UniTask` to `void` to reflect its synchronous nature. Replace `await loader.UnloadAssetAsync(x);` with `loader.UnloadAsset(x);`. -- Code generated by `AddressableIdsGenerator` must be re-generated (updated emitted `using` statement) - -## [1.0.1] - 2026-01-14 - -**Changed**: -- Updated dependency `com.gamelovers.dataextensions` to `com.gamelovers.gamedata` -- Updated assembly definitions to reference `GameLovers.GameData` - -## [1.0.0] - 2026-01-11 - -**New**: -- Added *AGENTS.md* document to help guide AI coding assistants to understand and work with this package library -- Added an entire test suit of unit/integration/performance/smoke tests to cover all the code for all services in this package - -**Changed**: -- Changed *VersionServices* namespace from *GameLovers* to *GameLovers.Services* to maintain consistency with other services in the package. -- Made *CallOnSpawned*, *CallOnSpawned\*, and *CallOnDespawned* methods virtual in *ObjectPoolBase\* to allow derived pool classes to customize lifecycle callback behavior. - -**Fixed**: -- Fixed the *README.md* file to now follow best practices in OSS standards for Unity's package library projects -- Fixed linter warnings in *VersionServices.cs* (redundant field initialization, unused lambda parameter, member shadowing) -- Fixed *GameObjectPool* not invoking *IPoolEntitySpawn.OnSpawn()* and *IPoolEntityDespawn.OnDespawn()* on components attached to spawned GameObjects. - -## [0.15.1] - 2025-09-24 - -**New**: -- Added protected property for all private fields in *CommandService* for access to any game specific project inheritance - -## [0.15.0] - 2025-01-05 - -**New**: -- Added *StartDelayCall* method to *ICoroutineService* to allow deferred methods to be safely executed within the bounds of a Unity Coroutine -- Added the possibility to know the current state of an *IAsyncCoroutine* -- Added the access to the Sample Entity used to generete new entites within an *IObjectPool* and destroy it when disposing the object pool -- Added the possibility to reset an *IObjectPool* to a new state - -## [0.14.1] - 2024-11-30 - -**Fixed**: -- Fixed the *RngLogic.Range(float, float, bool)* method to allow having the same min and max values with maxInclusive set to true - -## [0.14.0] - 2024-11-15 - -**New**: -- Added *PublishSafe* method to *IMessageBrokerService* to allow publishing messages safely in chase of chain subscriptions during publishing of a message - -**Changed**: -- *Subscribe* and *Unsubscribe* throw an *InvalidOperationException* when being executed during a message being published - -**Fixed**: -- CoroutineTests issues running when building released projects - -## [0.13.1] - 2024-11-04 - -**Fixed**: -- Fixed the *IInstaller* when trying to bind multiple interfaces at the same time - -## [0.13.0] - 2024-11-04 - -**Changed**: -- Changed *CommandService* to now receive the *MessageBrokerService* in the command and help communication with the game architecture - -## [0.12.2] - 2024-11-02 - -**Fixed**: -- Fixed an inssue where *IPoolEntityObject.Init()* wouldn't be called when spawning entities - -## [0.12.1] - 2024-10-25 - -**Fixed**: -- The endless loop when calling *RngService.Range()* -- The endless loop *GameObjectPool* when spawning new entities - -## [0.12.0] - 2024-10-22 - -**New**: -- Added *IRngData* to *PoolService* to suppprt read only data structure and allow abtract injection of data into other objects - -**Changed**: -- Changed *RngData* to a class in orther to avoid boxing/unboxing performance when injecting *IRngData*. - -## [0.11.0] - 2024-10-19 - -**New**: -- Added *Spawn(T data)* method to *PoolService* to allow spawning new objects with defined spawning data -- Added *GetPool()* && *TryGetPool()* methods to *PoolService* to allow requesting the pool object maintained by the pool service. - -**Changed**: -- Removed *IsSpawned()* method from *PoolService* because is not a fundamental function and can now be accessed from the Pool requested from *GetPool()* -- Now *Spawn(T data)* also invokes *OnSpawn()* without data so objects that implement *IPoolEntitySpawn* have the entire behaviour lifecycle - -## [0.10.0] - 2024-10-11 - -**New**: -- Updated *CommandService* to allow non struct type commands to be executed for reference type commands -- Added *Spawn(T data)* method to pool object to allow spawning new objects with defined spawning data - -## [0.9.0] - 2024-08-10 - -**New**: -- Updated interfaces and classes related to data services, enhancing modularity and improving version handling. -- Added classes for Git commands, version management, and random number generation. - -**Changed**: -- Restructured the data service interfaces, consolidating functionality into a single *IDataService* interface and removing unnecessary interfaces. -- Changed *AddData* to *AddOrReplaceData* in the *DataService* implementation. -- Removed the *isLocal* state from data handling. - -## [0.8.1] - 2023-08-27 - -**New**: -- Added GitEditorProcess class to run Git commands as processes, enabling checks for valid Git repositories, retrieving current branch names, commit hashes, and diffs from given commits. -- Introduced *VersionEditorUtils* class for managing application versioning. This includes setting and saving the internal version before building, loading version data from disk, and generating an internal version suffix based on Git information and build settings. - -**Changed**: -- Enhanced *IInstaller* interface with new methods for binding multiple type interfaces to a single instance, improving modularity and code organization. - -## [0.8.0] - 2023-08-05 - -**New**: -- Introduced *MainInstaller*, a singleton class for managing instances in the project. -- Added *RngService* for generating and managing random numbers. -- Implemented VersionServices to manage application version, including asynchronous loading of version data and comparison of version strings. - -## [0.7.1] - 2023-07-28 - -**Changed**: -- Tests have been moved to proper folders, and the package number has been updated. -- An unused namespace import has been removed from the InstallerTest class. - -**Fixed**: -- Compilation errors in various test files and the PoolService class have been fixed. - -## [0.7.0] - 2023-07-28 - -**New**: -- Introduced a code review process using GitHub Actions workflow. -- Added *IInstaller* interface and Installer implementation for binding and resolving instances. -- Updated namespaces, removed unused code, and modified method calls in test classes. - -**Changed**: -- Removed dependency on *ICommandNetworkService *and SendCommand method in *CommandService*. -- Updated *IDataService* interface and *DataService* class to handle local and online data saving. -- Improved readability of *MessageBrokerService* class by using var for type inference. -- Removed unused network service related interfaces, classes, and methods. -- Modified calculation of overFlow in TickService to check for zero DeltaTime. - -## [0.6.2] - 2020-09-10 - -**Changed**: -- Made *NetworkService* abstract and removed *INetworkService* to make easier to work with -- Improved Readme documentation - -## [0.6.1] - 2020-09-09 - -**New**: -- Added connection between *NetworkService* & *CommandService* -- Added integration tests - -## [0.6.0] - 2020-09-09 - -**New**: -- Added *NetworkService* -- Improved Readme documentation - -## [0.5.0] - 2020-07-10 - -**Changed**: -- Renamed *IDataWriter* and it's *FlushData* methods to *IDataSaver* & *SaveData* respectively to match with it's execution logic scope -- Moved the *AddData* to the *IDataService* to allow the *IDataSaver* have the single responsibility of saving data into disk - -## [0.4.1] - 2020-07-09 - -**New**: -- Added *CommandService* - -## [0.4.0] - 2020-07-09 - -**New**: -- Added *DataService* - -## [0.3.1] - 2020-02-25 - -**Fixed**: -- Fixed object pool despawn all elements. It was not despawning all the elements -- Fixed issue preventing to stop coroutines and thrown MissingReferenceException - -## [0.3.0] - 2020-02-09 - -**Changed**: -- Now the *MainInstaller* checks the object binding relationship in compile time -- Improved the *ObjectPools* helper classes with a now static global instatiator for game objects. -- Now the *PoolService* is only a service container for objects pools and no longer creates/initializes new pools. -- Removed *Pool.Clear* functionality. Use *DespawnAll* or delete the pool instead - -**Fixed**: -- The *CoroutineService* no longer fails on null coroutines - -## [0.2.0] - 2020-01-19 - -- Added new *ObjectPool* & *GameObjectPool* pools to allow to allow to use object pools independent from the *PoolService*. This allows to have different pools of the same type in the project in different object controllers -- Added new interface *IPoolEntityClear* that allows a callback method for entities when they are cleared from the pool -- Added new unit tests for the *ObjectPool* - -**Changed**: -- Now the *PoolService.Clear()* does not take any action parameters. To have a callback when the entity is cleared, please have the entity implement the *IPoolEntityClear* interface - -## [0.1.1] - 2020-01-06 - -**New**: -- Added License - -## [0.1.0] - 2020-01-06 - -- Initial submission for package distribution +# Changelog +All notable changes to this package will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [2.1.2] - 2026-08-04 + +**Changed**: +- Added the `com.unity.test-framework.performance` (3.5.0) dependency so the package's test assemblies compile when tests are enabled. + +**Fixed**: +- Fixed `GameObjectPool.Dispose(false)` and its generic equivalent so they preserve the sample entity when requested. +- Fixed pooling of Unity objects so destroyed pooled `GameObject` and `Behaviour` instances are skipped instead of being returned to callers. +- Fixed Addressable ID enum generation so addresses that sanitize to the same C# identifier receive distinct members. +- Fixed the Services Playground Input System setup by assigning default actions when its UI module is created, so sample controls respond as expected. + +## [2.1.1] - 2026-07-04 + +**Changed**: +- PlayMode tests updated to parameterless `FindObjectsByType()` overload (Unity 6 API). +- Removed redundant `[Serializable]` from `AddressableConfig` (already implicitly serializable as a reference type in Unity YAML). + +**Fixed**: +- `ServicesScaffolders` adapts to Unity 6000.4+ `AssetCreationEndAction` / `EntityId` API (guarded by `UNITY_6000_4_OR_NEWER`; pre-6000.4 path unchanged). + +## [2.1.0] - 2026-05-20 + +**New**: +- `VersionServices` now auto-bootstraps via `[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]`, populating version metadata before any scene `Awake` callback and before vendor-SDK `SubsystemRegistration` callbacks that read it. Consumers no longer need to call `LoadVersionData()` / `LoadVersionDataAsync()` explicitly for the default flow. + +**Changed**: +- Property getters (`VersionInternal`, `Branch`, `Commit`, `BuildNumber`) now lazy-load via a new private `EnsureLoaded()` on first access if the auto-bootstrap hook has not yet fired — protects against undefined ordering between sibling assemblies' `[RuntimeInitializeOnLoadMethod]` callbacks at the same phase. +- Removed the private `IsLoaded()` helper (replaced by `EnsureLoaded()` invoked from each property getter). + +**Docs**: +- `docs/version-services.md` rewritten around the new auto-bootstrap contract: the recommended usage no longer includes any explicit load call, the lazy-load fallback is documented, and the Error Reference table now describes the fallback behaviour (no exception is raised). + +## [2.0.2] - 2026-05-20 + +**Fixed**: +- Add missing meta file + +## [2.0.1] - 2026-05-20 + +**New** +- Added new test suite for more rebust code coverage +- Added `VersionServices.LoadVersionData()` — synchronous sibling of `LoadVersionDataAsync()` for consumers who want to populate version metadata at boot without an `await`. Both methods now funnel into a shared private `ApplyTextAsset` helper, so behaviour is identical. Sync is the recommended default for the shipping `version-data.txt` (a few hundred bytes); async remains available for cases where `VersionData` is extended with large embedded blobs. Covered by `Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs`. + +**Fixed**: +- `Tests/AGENTS.md` §8 extended with a new **"Authorized reflection sites (storage-assertion exception)"** subsection. + +## [2.0.0] - 2026-04-26 + +**New**: +- Added **Services Explorer** window (`Tools > GameLovers > Services Explorer`) with 13 live-refresh tabs: Overview, Installer, MessageBroker, Tick, Coroutine, Pool, Data, Time, RNG, AssetResolver, Versioning, Assets Importer, Addressable Ids — works in both Edit and Play mode +- Menu stubs under `Tools > GameLovers`: + - `Versioning / Refresh Version Data` and `Versioning / Open in Explorer` + - `Assets Importer / Import Assets Data` and `Assets Importer / Open in Explorer` + - `Addressable Ids / Generate Addressable Ids` and `Addressable Ids / Open in Explorer` +- Added `Assets > Create > GameLovers Services > …` scaffolders: Message, Command, Service, Pool Entity (template-based, $NAME$ / $NAMESPACE$ substitution) +- Absorbed `com.gamelovers.assetsimporter` v0.5.2 into this package +- Added `IAssetLoader`, `ISceneLoader`, `AddressablesAssetLoader` to `Runtime/AssetsImporter/` (ns `GameLovers.Services.AssetsImporter`) +- Added `AddressableConfig`, `AssetConfigsScriptableObject`, `AssetLoaderUtils`, `AssetReferenceScene` to `Runtime/AssetsImporter/` (ns `GameLovers.Services.AssetsImporter`) +- Added `AssetResolverService` (implements `IAssetResolverService` / `IAssetAdderService`) to `Runtime/` root (ns `GameLovers.Services`) +- Added Editor/AssetsImporter/: `AssetsImporter`, `AssetsToolImporter`, `AssetConfigsImporter`, `AddressableIdsGenerator`, `AddressablesIdGeneratorSettings` (ns `GameLovers.Services.AssetsImporter.Editor`) +- Added importable **Samples** under `Samples~/` (importable via Unity Package Manager > GameLovers Services > Samples). + - **Services Playground** — single-scene, zero-setup walk-through that wires every foundation service via `MainInstaller` and exercises 10 of 13 Services Explorer tabs end-to-end. + - **Asset Resolver** — focused demo of `AssetResolverService` end-to-end (`AddConfigs` / `RequestAsset` / `UnloadAssets`) with `SpriteConfigs : AssetConfigsScriptableObject`. Drives the three Services Explorer tabs the Playground does not cover (Asset Resolver, Assets Importer, Addressable Ids). + +**Changed**: +- Addressable Ids generator and Assets Importer settings moved from `Assets/*.asset` ScriptableObjects to `ProjectSettings/` ScriptableSingletons (mirrors `VersioningEditorSettings`). +- Generation logic extracted from `AddressableIdsGenerator` into `AddressableIdsGeneratorUtils` (static `internal`); importer discovery/import logic extracted into `AssetsImporterEditorUtils`. +- `Tools/Assets Importer/*` and `Tools/AddressableIds Generator/*` menu entries removed. Use `Tools/GameLovers/Assets Importer/...`, `Tools/GameLovers/Addressable Ids/...`, or the Services Explorer tabs instead. +- `Toggle Auto Import On Refresh` menu entry removed. The toggle now lives exclusively in the Services Explorer **Assets Importer** tab. +- `Assets/AssetsImporter.asset` and `Assets/AddressablesIdGeneratorSettings.asset` are no longer used (settings moved to `ProjectSettings/`); safe to delete from consumer projects. +- Deleted editor source files: `AssetsImporter.cs`, `AssetsToolImporter.cs`, `AddressableIdsGenerator.cs`, `AddressablesIdGeneratorSettings.cs`. +- Folder reorganization: `Runtime/` now has domain subfolders `DependencyInjection/`, `Commands/`, `Pooling/`, `AssetsImporter/`; `Editor/` now has `Versioning/` and `AssetsImporter/` subfolders +- `Installer.cs` and `MainInstaller.cs` moved to `Runtime/DependencyInjection/` (namespace unchanged: `GameLovers.Services`) +- `CommandService.cs` trimmed to concrete class only; command contract interfaces extracted to `Runtime/Commands/` under ns `GameLovers.Services.Commands` +- `PoolService.cs` trimmed to concrete class only; pool interfaces + implementations moved to `Runtime/Pooling/` under ns `GameLovers.Services.Pooling` +- `ObjectPool.cs` (578 lines, 10 types) split into 4 files under `Runtime/Pooling/`: `IPoolEntity.cs`, `IObjectPool.cs`, `ObjectPool.cs`, `GameObjectPool.cs` +- `VersionEditorUtils.cs` and `GitEditorProcess.cs` moved to `Editor/Versioning/`; re-namespaced from `GameLovers.Services.Editor` → `GameLovers.Services.Versioning.Editor` +- Added new hard dependencies: `com.unity.addressables` (1.21.20) and `com.cysharp.unitask` (2.5.10) + +**Fixed**: +- `AddressablesAssetLoader.UnloadAsset` no longer calls `GC.Collect()`, `GC.WaitForPendingFinalizers()`, or `Resources.UnloadUnusedAssets()`. The method now only decrements the Addressables reference count. The old implementation caused PlayMode Test Runner crashes on macOS and O(total-assets-in-memory) main-thread stalls per per-asset release. Callers that need memory reclamation should invoke `Resources.UnloadUnusedAssets()` themselves at appropriate moments (scene transitions, boot, memory-pressure events); Unity also runs an unused-assets sweep automatically on `LoadSceneMode.Single` scene loads. +- Corrected `IAssetLoader.UnloadAsset` XML documentation: removed the incorrect "will also destroy GameObject instances" claim — `Addressables.Release(gameObject)` does not destroy the instance; callers must `Object.Destroy` it separately. +- `IAsyncCoroutine.StopCoroutine(bool triggerOnComplete)` now honors its `triggerOnComplete` parameter and flips `IsCompleted` to `true` and `IsRunning` to `false` after stopping. The previous implementation always invoked `OnComplete` callbacks regardless of the flag and left state flags unchanged, so consumers could not distinguish a stopped coroutine from a running one and `triggerOnComplete: false` was silently ignored. +- `GameObjectPool.Dispose()` and `GameObjectPool.Dispose()` now skip pooled entries whose underlying `GameObject` has already been destroyed by an external owner (e.g. a parent GameObject was destroyed while pooled instances were still reparented under it via `DespawnToSampleParent`). + +**Breaking Changes** — see `MIGRATION.md` for details: +- Pool types moved from `GameLovers.Services` to `GameLovers.Services.Pooling` (`IPoolService`, `IObjectPool`, `IObjectPool`, `ObjectPool`, `ObjectPoolBase`, `GameObjectPool`, `GameObjectPool`, `IPoolEntitySpawn`, `IPoolEntitySpawn`, `IPoolEntityDespawn`, `IPoolEntityObject`). `PoolService` concrete class remains in `GameLovers.Services`. +- Command contract types moved from `GameLovers.Services` to `GameLovers.Services.Commands` (`IGameCommandBase`, `IGameCommand<>`, `IGameServerCommand<>`, `ICommandService<>`). `CommandService<>` concrete class remains in `GameLovers.Services`. +- `GameLovers.AssetsImporter.*` renamed to `GameLovers.Services.AssetsImporter.*` +- `GameLovers.AssetsImporter.AssetResolverService` is now `GameLovers.Services.AssetResolverService` +- `GameLovers.Services.Editor.*` (versioning editor) renamed to `GameLovers.Services.Versioning.Editor.*` +- `IAssetLoader.UnloadAssetAsync(T, Action)` → `IAssetLoader.UnloadAsset(T, Action)`: method renamed (dropped `Async` suffix) and return type changed from `UniTask` to `void` to reflect its synchronous nature. Replace `await loader.UnloadAssetAsync(x);` with `loader.UnloadAsset(x);`. +- Code generated by `AddressableIdsGenerator` must be re-generated (updated emitted `using` statement) + +## [1.0.1] - 2026-01-14 + +**Changed**: +- Updated dependency `com.gamelovers.dataextensions` to `com.gamelovers.gamedata` +- Updated assembly definitions to reference `GameLovers.GameData` + +## [1.0.0] - 2026-01-11 + +**New**: +- Added *AGENTS.md* document to help guide AI coding assistants to understand and work with this package library +- Added an entire test suit of unit/integration/performance/smoke tests to cover all the code for all services in this package + +**Changed**: +- Changed *VersionServices* namespace from *GameLovers* to *GameLovers.Services* to maintain consistency with other services in the package. +- Made *CallOnSpawned*, *CallOnSpawned\*, and *CallOnDespawned* methods virtual in *ObjectPoolBase\* to allow derived pool classes to customize lifecycle callback behavior. + +**Fixed**: +- Fixed the *README.md* file to now follow best practices in OSS standards for Unity's package library projects +- Fixed linter warnings in *VersionServices.cs* (redundant field initialization, unused lambda parameter, member shadowing) +- Fixed *GameObjectPool* not invoking *IPoolEntitySpawn.OnSpawn()* and *IPoolEntityDespawn.OnDespawn()* on components attached to spawned GameObjects. + +## [0.15.1] - 2025-09-24 + +**New**: +- Added protected property for all private fields in *CommandService* for access to any game specific project inheritance + +## [0.15.0] - 2025-01-05 + +**New**: +- Added *StartDelayCall* method to *ICoroutineService* to allow deferred methods to be safely executed within the bounds of a Unity Coroutine +- Added the possibility to know the current state of an *IAsyncCoroutine* +- Added the access to the Sample Entity used to generete new entites within an *IObjectPool* and destroy it when disposing the object pool +- Added the possibility to reset an *IObjectPool* to a new state + +## [0.14.1] - 2024-11-30 + +**Fixed**: +- Fixed the *RngLogic.Range(float, float, bool)* method to allow having the same min and max values with maxInclusive set to true + +## [0.14.0] - 2024-11-15 + +**New**: +- Added *PublishSafe* method to *IMessageBrokerService* to allow publishing messages safely in chase of chain subscriptions during publishing of a message + +**Changed**: +- *Subscribe* and *Unsubscribe* throw an *InvalidOperationException* when being executed during a message being published + +**Fixed**: +- CoroutineTests issues running when building released projects + +## [0.13.1] - 2024-11-04 + +**Fixed**: +- Fixed the *IInstaller* when trying to bind multiple interfaces at the same time + +## [0.13.0] - 2024-11-04 + +**Changed**: +- Changed *CommandService* to now receive the *MessageBrokerService* in the command and help communication with the game architecture + +## [0.12.2] - 2024-11-02 + +**Fixed**: +- Fixed an inssue where *IPoolEntityObject.Init()* wouldn't be called when spawning entities + +## [0.12.1] - 2024-10-25 + +**Fixed**: +- The endless loop when calling *RngService.Range()* +- The endless loop *GameObjectPool* when spawning new entities + +## [0.12.0] - 2024-10-22 + +**New**: +- Added *IRngData* to *PoolService* to suppprt read only data structure and allow abtract injection of data into other objects + +**Changed**: +- Changed *RngData* to a class in orther to avoid boxing/unboxing performance when injecting *IRngData*. + +## [0.11.0] - 2024-10-19 + +**New**: +- Added *Spawn(T data)* method to *PoolService* to allow spawning new objects with defined spawning data +- Added *GetPool()* && *TryGetPool()* methods to *PoolService* to allow requesting the pool object maintained by the pool service. + +**Changed**: +- Removed *IsSpawned()* method from *PoolService* because is not a fundamental function and can now be accessed from the Pool requested from *GetPool()* +- Now *Spawn(T data)* also invokes *OnSpawn()* without data so objects that implement *IPoolEntitySpawn* have the entire behaviour lifecycle + +## [0.10.0] - 2024-10-11 + +**New**: +- Updated *CommandService* to allow non struct type commands to be executed for reference type commands +- Added *Spawn(T data)* method to pool object to allow spawning new objects with defined spawning data + +## [0.9.0] - 2024-08-10 + +**New**: +- Updated interfaces and classes related to data services, enhancing modularity and improving version handling. +- Added classes for Git commands, version management, and random number generation. + +**Changed**: +- Restructured the data service interfaces, consolidating functionality into a single *IDataService* interface and removing unnecessary interfaces. +- Changed *AddData* to *AddOrReplaceData* in the *DataService* implementation. +- Removed the *isLocal* state from data handling. + +## [0.8.1] - 2023-08-27 + +**New**: +- Added GitEditorProcess class to run Git commands as processes, enabling checks for valid Git repositories, retrieving current branch names, commit hashes, and diffs from given commits. +- Introduced *VersionEditorUtils* class for managing application versioning. This includes setting and saving the internal version before building, loading version data from disk, and generating an internal version suffix based on Git information and build settings. + +**Changed**: +- Enhanced *IInstaller* interface with new methods for binding multiple type interfaces to a single instance, improving modularity and code organization. + +## [0.8.0] - 2023-08-05 + +**New**: +- Introduced *MainInstaller*, a singleton class for managing instances in the project. +- Added *RngService* for generating and managing random numbers. +- Implemented VersionServices to manage application version, including asynchronous loading of version data and comparison of version strings. + +## [0.7.1] - 2023-07-28 + +**Changed**: +- Tests have been moved to proper folders, and the package number has been updated. +- An unused namespace import has been removed from the InstallerTest class. + +**Fixed**: +- Compilation errors in various test files and the PoolService class have been fixed. + +## [0.7.0] - 2023-07-28 + +**New**: +- Introduced a code review process using GitHub Actions workflow. +- Added *IInstaller* interface and Installer implementation for binding and resolving instances. +- Updated namespaces, removed unused code, and modified method calls in test classes. + +**Changed**: +- Removed dependency on *ICommandNetworkService *and SendCommand method in *CommandService*. +- Updated *IDataService* interface and *DataService* class to handle local and online data saving. +- Improved readability of *MessageBrokerService* class by using var for type inference. +- Removed unused network service related interfaces, classes, and methods. +- Modified calculation of overFlow in TickService to check for zero DeltaTime. + +## [0.6.2] - 2020-09-10 + +**Changed**: +- Made *NetworkService* abstract and removed *INetworkService* to make easier to work with +- Improved Readme documentation + +## [0.6.1] - 2020-09-09 + +**New**: +- Added connection between *NetworkService* & *CommandService* +- Added integration tests + +## [0.6.0] - 2020-09-09 + +**New**: +- Added *NetworkService* +- Improved Readme documentation + +## [0.5.0] - 2020-07-10 + +**Changed**: +- Renamed *IDataWriter* and it's *FlushData* methods to *IDataSaver* & *SaveData* respectively to match with it's execution logic scope +- Moved the *AddData* to the *IDataService* to allow the *IDataSaver* have the single responsibility of saving data into disk + +## [0.4.1] - 2020-07-09 + +**New**: +- Added *CommandService* + +## [0.4.0] - 2020-07-09 + +**New**: +- Added *DataService* + +## [0.3.1] - 2020-02-25 + +**Fixed**: +- Fixed object pool despawn all elements. It was not despawning all the elements +- Fixed issue preventing to stop coroutines and thrown MissingReferenceException + +## [0.3.0] - 2020-02-09 + +**Changed**: +- Now the *MainInstaller* checks the object binding relationship in compile time +- Improved the *ObjectPools* helper classes with a now static global instatiator for game objects. +- Now the *PoolService* is only a service container for objects pools and no longer creates/initializes new pools. +- Removed *Pool.Clear* functionality. Use *DespawnAll* or delete the pool instead + +**Fixed**: +- The *CoroutineService* no longer fails on null coroutines + +## [0.2.0] - 2020-01-19 + +- Added new *ObjectPool* & *GameObjectPool* pools to allow to allow to use object pools independent from the *PoolService*. This allows to have different pools of the same type in the project in different object controllers +- Added new interface *IPoolEntityClear* that allows a callback method for entities when they are cleared from the pool +- Added new unit tests for the *ObjectPool* + +**Changed**: +- Now the *PoolService.Clear()* does not take any action parameters. To have a callback when the entity is cleared, please have the entity implement the *IPoolEntityClear* interface + +## [0.1.1] - 2020-01-06 + +**New**: +- Added License + +## [0.1.0] - 2020-01-06 + +- Initial submission for package distribution From bdf9bb3edcdda42fa8c70d7a8b813eaa000ee862 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 23:29:46 +0100 Subject: [PATCH 31/32] build: exclude .github/ from the published tarball Unity's packer uses .gitignore as its pack-ignore list, so listing .github/ drops the CI workflow from the tarball while git keeps tracking it (gitignore does not untrack existing files). Verified on a real clone: 434 -> 433 entries, .github 1 -> 0, Runtime unchanged. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 0c49860..30aeff1 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,6 @@ crashlytics-build.properties # Tests audit history (unity-tests-audit skill -- local developer state, never committed) .audit-history.md + +# CI config: tracked in git, excluded from the published UPM tarball +.github/ From 528b939bfe9beb31afe4ffa4b841fe538ba9efde Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Tue, 4 Aug 2026 23:42:05 +0100 Subject: [PATCH 32/32] ci: add release-preflight check for develop->master PRs --- .github/workflows/release-preflight.yml | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-preflight.yml b/.github/workflows/release-preflight.yml index 1300736..005ac2f 100644 --- a/.github/workflows/release-preflight.yml +++ b/.github/workflows/release-preflight.yml @@ -10,13 +10,17 @@ # package-local gates the release driver enforces, run before the merge rather # than after it. # -# WHY IT IS SAFE TO SHIP IN A PACKAGE REPO -# Verified against upm 9.31.1 with a positive control: `.github/` is excluded -# from the packed tarball automatically, even when it is not gitignored, while a -# non-ignored control file at the package root IS packed. (Older packers did not -# exclude it — the published googlesheetimporter 0.7.2 asset still contains -# `package/.github/workflows/openai.yml`.) So no `.npmignore` is required. If -# that ever regresses, `G24` will surface it as an unexpected added file. +# THIS FILE MUST STAY OUT OF THE PUBLISHED TARBALL +# `.github/` IS packed by default — the published googlesheetimporter 0.7.2 asset +# still contains `package/.github/workflows/openai.yml`. Each package therefore +# lists `.github/` in its `.gitignore`, which Unity's packer uses as its +# pack-ignore list; git keeps tracking the file regardless, since .gitignore does +# not untrack existing paths. Measured on a real clone: 434 -> 433 entries, +# `.github` 1 -> 0, `Runtime` unchanged. +# +# Do NOT verify this on a copy with `.git` removed: the packer behaves differently +# without a repo and reports `.github` as excluded when it is not. `G24` also +# surfaces it as an unexpected added file if the ignore line is ever dropped. # # WHAT IT DOES NOT CHECK # Only gates decidable from the package directory plus the base ref, so the check @@ -43,10 +47,16 @@ jobs: preflight: runs-on: ubuntu-latest steps: + # `lfs: true` is load-bearing, not a nicety. The default (false) leaves every + # LFS-tracked file as a ~130-byte pointer stub, which would make G28 fail on + # every PR. With it on, G28 becomes a genuine check that the LFS objects are + # FETCHABLE from the remote — the exact failure that hit uiservice, where the + # working tree held stubs while the published 1.2.1 had real content. - name: Check out the package uses: actions/checkout@v4 with: fetch-depth: 0 + lfs: true persist-credentials: false # The gate logic is shared rather than vendored into six repos, so there is