diff --git a/Directory.Build.props b/Directory.Build.props index 49e5141..a651f51 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -20,7 +20,7 @@ $(WarningsNotAsErrors);NU1900;NU1901;NU1902;NU1905 x64 - 2.0.0-beta.3 + 2.0.0-beta.4 CheatEngineNet CheatEngineNet https://github.com/CheatEngineNet/CheatEngine.Mcp diff --git a/Directory.Packages.props b/Directory.Packages.props index 1d3f278..0036d32 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,8 +4,8 @@ true - - + + diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index d84de80..160164f 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -12,13 +12,13 @@ shipped package has no row. | Package | Version | License | Copyright | Shipped in | License text | |---|---|---|---|---|---| -| CheatEngine.Client | 1.0.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Metapackage, no files | [MIT License](#mit-license) | -| CheatEngine.Client.Abstractions | 1.0.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | -| CheatEngine.Client.Core | 1.0.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | -| CheatEngine.Client.Extensions.DependencyInjection | 1.0.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | -| CheatEngine.Client.Fluent | 1.0.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | -| CheatEngine.Client.Hosting | 1.0.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | -| CheatEngine.SDK | 2.0.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.SDK contributors | Embedded plugin dependencies: the seven `CheatEngine.SDK*.dll` assemblies and the native `cheatengine-sdk-lua-bridge.dll`. Gateway: the managed assemblies only | [MIT License](#mit-license) | +| CheatEngine.Client | 1.1.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Metapackage, no files | [MIT License](#mit-license) | +| CheatEngine.Client.Abstractions | 1.1.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | +| CheatEngine.Client.Core | 1.1.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | +| CheatEngine.Client.Extensions.DependencyInjection | 1.1.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | +| CheatEngine.Client.Fluent | 1.1.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | +| CheatEngine.Client.Hosting | 1.1.0 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.Client contributors | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | +| CheatEngine.SDK | 2.0.1 | MIT | Copyright (c) 2026 AriusII, ShadowNineX and CheatEngine.SDK contributors | Embedded plugin dependencies: the seven `CheatEngine.SDK*.dll` assemblies and the native `cheatengine-sdk-lua-bridge.dll`. Gateway: the managed assemblies only | [MIT License](#mit-license) | | Costura.Fody | 6.2.0 | MIT | Copyright (c) 2012 Simon Cropp and contributors | Plugin: embedded dependencies and injected loader code | [MIT License](#mit-license) | | Microsoft.Extensions.AI.Abstractions | 10.10.1 | MIT | Copyright (c) .NET Foundation; © Microsoft Corporation | Embedded plugin dependencies, gateway | [MIT License](#mit-license) | | ModelContextProtocol | 2.2.0 | Apache-2.0 (MIT for earlier contributions not yet relicensed) | © Model Context Protocol a Series of LF Projects, LLC. | Embedded plugin dependencies, gateway | [Apache License 2.0](#apache-license-20), [MIT License](#mit-license) | @@ -62,8 +62,8 @@ SDK changes the runtime version, update this table. | Component | Upstream license file | |---|---| -| CheatEngine.Client 1.0.0 | (the source commit of the packages) | -| CheatEngine.SDK 2.0.0 | (the source commit of the package) | +| CheatEngine.Client 1.1.0 | (the source commit of the packages) | +| CheatEngine.SDK 2.0.1 | (the source commit of the package) | | Microsoft.Extensions.AI.Abstractions 10.10.1 | (the source commit of the package) | | ModelContextProtocol 2.2.0 | and (the source commit of the packages) | | .NET runtime packs 10.0.12 | `LICENSE.TXT` and `THIRD-PARTY-NOTICES.TXT` of the runtime packs | diff --git a/docs/beta4-release-notes.md b/docs/beta4-release-notes.md new file mode 100644 index 0000000..807b6e7 --- /dev/null +++ b/docs/beta4-release-notes.md @@ -0,0 +1,24 @@ +# CheatEngine.Mcp 2.0.0-beta.4 + +This Windows x64 prerelease updates the Client/SDK dependencies, fixes two tool adapters and expands automated coverage across the complete MCP catalog. Download `CheatEngine.Mcp-2.0.0-beta.4-win-x64.zip` and verify it with `SHA256SUMS.txt`. The ZIP contains the single plugin DLL, matching gateway executable, installation instructions and licenses. + +## Changes since beta.3 + +- Updated `CheatEngine.Client` to 1.1.0 and `CheatEngine.SDK` to 2.0.1, including regenerated normal and Native AOT dependency locks. +- Fixed `code_get_comments` so its address array reaches Lua as one argument, and fixed `process_list_threads` to use CE's documented zero-based StringList indexer. +- Added catalog-derived routing/refusal checks for every backend tool, positive adapter evidence for all 193 backend tools, and gateway-local discovery checks. New catalog entries must have reviewed adapter-test mappings. +- Expanded standalone Lua adapter tests and execution-argument, conversion/calculation and pointer-file boundary, failure and cleanup tests. +- Added a bounded live workflow scenario that tests target, gateway and host restarts independently of the unresolved plugin reload bridge. +- Preserved the latest test-platform package updates from `main`. + +## Validation and known limits + +The recorded automated baseline passes 5,314 portable tests in both Debug and Release and 651 standalone Lua tests, with no failures or skips. Combined coverage reaches 94.50% of instrumented handwritten product C# lines, including 94.37% in Tools. This C# percentage does not measure embedded Lua branch coverage or native CE behavior. + +The release pipeline checks the isolated bundled DLL, reflected catalog, Native AOT gateway smoke responses, ZIP contents and checksums. Native evidence for 37 backend tools comes from the separately identified private dependency-refresh candidate recorded in the [all-tool qualification report](https://github.com/CheatEngineNet/CheatEngine.Mcp/blob/main/docs/qualification/all-tools/README.md); it does not qualify newly rebuilt release bytes. Another 156 backend tools still need native success fixtures, and same-process plugin reload remains unqualified. SDK provider-stack guards reject invalid states; the original intermittent dispatcher corruption was investigated, but its root cause remains unresolved. This beta is not stable v2.0.0 sign-off. + +## Installation, upgrade and rollback + +Use Cheat Engine 7.7 x64 with the x64 .NET 10, ASP.NET Core 10 and Windows Desktop 10 runtimes. The gateway is self-contained. All four optional execution capabilities remain enabled by default; configure their exposure for your setup. The recorded bounded native workflows explicitly disabled them. + +Back up the previous matching plugin/gateway pair, configuration and CE settings. Disable the plugin and close CE and the gateway before extracting beta.4 into a new folder. Install `CheatEngine.Mcp.dll`, point the MCP client at the matching `CheatEngine.Mcp.Gateway.exe`, then use `instance_list` to discover a fresh instance ID. Keep both binaries from the same ZIP. To roll back, close both processes and restore the previous matching pair and registration. See the [recovery guide](https://github.com/CheatEngineNet/CheatEngine.Mcp/blob/main/srcs/CheatEngine.Mcp.Resources/Knowledge/Documents/connection-troubleshooting.md#several-instances-updates-and-removal) for locked files and partial upgrades. diff --git a/docs/qualification/all-tools/README.md b/docs/qualification/all-tools/README.md new file mode 100644 index 0000000..93acca5 --- /dev/null +++ b/docs/qualification/all-tools/README.md @@ -0,0 +1,224 @@ +# Full tool qualification + +The current catalog contains 193 backend tools and the gateway's `instance_list` tool. +Every tool needs a reviewed success case with known inputs, an independent result or side-effect oracle, +and cleanup verification. Catalog discovery, routing, disabled-feature refusals and compilation of a fixed +Lua body do not establish successful native execution. + +## Checklist + +- [x] Enumerate the complete advertised catalog. +- [x] Check routing, missing/unknown instance refusal and stale-identity refusal for every backend tool. +- [x] Check gateway-local discovery with zero and two instances. +- [x] Run the portable Release suite; final results are recorded below. +- [x] Run the portable Debug suite with the same exclusions. +- [x] Run standalone Lua 5.3 with CE API doubles (651 passed, zero failed or skipped). +- [x] Add and run result assertions for the Lua, table-save and code-view adapters (12 passed). +- [x] Add seven record adapter cases and four code/process adapter cases; run both focused classes. +- [x] Fix the `code_get_comments` array argument expansion found by those tests. +- [x] Fix `process_list_threads` to use CE's documented zero-based StringList indexer; verify empty, truncated and error cleanup paths. +- [x] Prepare eight additional bounded live cases and review their assertions. +- [x] Attempt the private x64 lifecycle session against the published beta.3 bundle. +- [x] Verify both private hosts stopped, user state was restored and the installed CE files stayed unchanged. +- [ ] Qualify same-process plugin reload: the Settings bridge observes zero plugin rows after `loadPlugin` succeeded. +- [x] Add a separate disabled-capability workflow scenario so independent restart checks can complete. +- [x] Run the eight added live checks and six bounded workflows, then target, gateway and host restarts. +- [x] Map a positive production-adapter result/effect assertion for every backend tool. +- [x] Add SDK provider-stack containment and normal-path preservation regressions. +- [x] Exercise the SDK dispatcher with rooted worker/main coroutines, including queued-stop cleanup. +- [x] Prepare, review and run a bounded provider trace replay; retain non-reproduction as diagnostic evidence only. +- [x] Add direct adapter checks for managed collectors, debugger callbacks, execution, kernel calls, patches and table loading using doubles. +- [x] Update to published Client 1.1.0 and SDK 2.0.1; regenerate portable and AOT lockfiles. +- [x] Require reviewed positive adapter mappings for every name in the production tool catalog. +- [x] Add execution-argument, conversion/calculation and pointer-file boundary/failure tests. +- [x] Measure handwritten product C# coverage using fresh portable and NativeLua reports. +- [x] Rebuild and repeat the disabled-capability workflows on the updated dependency bundle. +- [ ] Supply native managed collector, Mono, debugger event, compiler/ABI, PDB/export, table and patch fixtures. +- [ ] Qualify successful gated operations and DBK/DBVM operations in their required environments. +- [ ] Qualify all tools on the exact replacement product bundle after fixes, including restart and cleanup. + +The machine-readable report contains one row for each of the 194 tools. It keeps routing, tests using +doubles, direct fixed-Lua bodies and live results separate. The reviewed +[`behavior-evidence.json`](behavior-evidence.json) maps positive adapter evidence for all 193 backend names. +Each mapped test lists its exact required TRX case names; every case must occur once and pass. +Passing only one branch of a parameterized test cannot qualify another branch's tool. +These cases use Client or CE API doubles, including real standalone Lua where applicable. They are partial +cases, not complete behavior partitions or native CE qualification. The gateway's local discovery checks +bring the advertised catalog total to 194. A live mapping requires its own concrete oracle and cleanup receipts. + +## Automated results from 2026-10-16 + +The dependency refresh uses the published NuGet packages `CheatEngine.Client` **1.1.0** and +`CheatEngine.SDK` **2.0.1**. Both normal and AOT lockfiles were regenerated. Debug and Release builds +completed with zero warnings or errors. The isolated bundled-DLL reflection/load probe and the published +Native AOT gateway smoke check also passed. + +Fresh final TRX reports contain: + +| Suite | Passed | Failed | Skipped | +| --- | ---: | ---: | ---: | +| Portable Release | 5,314 | 0 | 0 | +| Portable Debug | 5,314 | 0 | 0 | +| Standalone NativeLua | 651 | 0 | 0 | +| Opt-in live Workflows session | 1 | 0 | 0 | + +The generated report verifies routing for all **194** advertised tools, positive adapter evidence for +all **193** backend tools, and the exact required parameterized cases. The added +`AllToolBehaviorEvidenceTests` derives its names from the production catalog and checks that every +adapter mapping references an existing public test, source file, oracle and case list. This structural +guard does not execute the mapped test or prove its outcome; the report checks the fresh TRX results. +The report records input hashes but does not independently attest that a supplied TRX came from current source. + +Additional tests pin execution argument types and numeric limits, indexed validation errors, timeout +endpoints and symbol-lookup failure semantics. Conversion/calculator tests cover byte order, fixed scalar +values, operator precedence, invalid expressions and resource limits. Pointer-file tests cover schema +and snapshot validation, incomplete coverage, chunked nonseekable reads, stream ownership and failures +before writes. These checks use deterministic inputs and result, error or side-effect assertions. + +Combined portable and standalone Lua coverage reaches **20,193 / 21,369 handwritten product C# lines +(94.50%)**. Hits are unioned by source file and line; generated files and test code are excluded. +This percentage does not measure embedded Lua branch coverage or prove native host behavior. + +| Product project | Covered / instrumented lines | Line coverage | +| --- | ---: | ---: | +| Core | 4,357 / 4,581 | 95.11% | +| Gateway entry point | 9 / 10 | 90.00% | +| Hosting | 1,228 / 1,348 | 91.10% | +| Plugin | 794 / 851 | 93.30% | +| Prompts | 704 / 709 | 99.29% | +| Resources | 222 / 222 | 100.00% | +| Tools | 12,879 / 13,648 | 94.37% | + +The separate static source-to-test pairing analyzer could not parse this repository's C# 14 extension +blocks with its bundled Roslyn 4.14. Its partial classifications are not used as coverage evidence. + +The current private x64 run `20261016T174241Z-e914` passed the eight additional checks, six bounded +workflows and one restart each of target A, gateway and host A. All four optional gates were explicitly +disabled, and zero plugin reload cycles were requested. The session passed in about 37 seconds, restored +user state, left the source installation unchanged and reported no cleanup failures. The runner also +confirmed that no recorded owned processes remained. Its 813 prepared inputs matched their manifest. + +The report verifies **37 backend tools** against this run's exact primary and cleanup receipts. +The other **156 backend tools** still lack native success evidence; gateway-local `instance_list` has +its separate discovery tests. Same-process plugin reload and the native fixtures listed above remain +unqualified. `nativeQualificationComplete` remains false. + +Current private bundle SHA-256 values: + +- DLL: `8C01D51F78642FF036E4616032A541D5DE2BD534006D9FF51C8C97B81FC33AE8` +- Gateway: `664EB1CD2756ED076C443446A6D09A151CEB30CA43BB69BC2392555C3BEFD2E3` + +Fresh TRX and Cobertura files are under `artifacts/test-results/all-tools-20261016/` in +`portable-reviewed`, `portable-debug-reviewed`, `native-reviewed` and `live`. The per-tool report is +`tool-report-final/tool-results.json`, and the deduplicated line totals are in +`product-coverage-final.json`. These local artifacts and raw native logs are not committed. +Installed and released binaries have not been replaced by this work. + +## Automated results from 2026-10-11 + +The final portable suites passed in both Debug and Release: 5,046 tests each, zero failures or skips. +The final standalone NativeLua suite passed 651 tests, zero failures or skips. The report generated from +the Release and NativeLua TRX files verifies positive adapter evidence for all 193 backend tools. +It also verifies routing for all backend tools and the gateway's own discovery cases. + +[`live-evidence.json`](live-evidence.json) maps 37 positive native tool cases from the reviewed workflow +session below. Each case requires its primary receipt and any mapped cleanup receipt, along with a passed +session, restored user state, an unchanged source installation and no cleanup failures. These 37 cases +do not establish native qualification of the entire catalog. + +## Failure observed on 2026-10-11 + +The ordinary x64 lifecycle session used the published `2.0.0-beta.3+059f249cc3293dff65b19ca257ecfe196d20aac1` +plugin and gateway, with all four optional gates disabled. Reading the second instance's runtime resource +failed with `The Lua stack does not contain the inputs required by the protected operation.` +The stack trace reaches `LuaProtectedApi.PushBytes`, `LuaState.TryGetGlobal` and +`MainThreadDispatcher.Dispatch` before the MCP operation body starts. This identifies the failure +boundary; it does not prove its underlying cause. No automatic native retry was made. + +The private run is `20261011T134351Z-8024`. Its final summary reports `passed: false`, +`userStateRestored: true`, `sourceInstallationUnchanged: true` and no cleanup failures. +The eight added live cases were not reached in that run. Raw logs and discovery material stay +in the private run directory; they are not committed. + +The `code_get_comments` fix is currently a source change. The installed/released beta.3 bundle has not +been replaced by this work. The regression checks two ordered addresses through the actual managed +adapter and real Lua 5.3 with CE API doubles, avoiding the array-shape bypass in the previous compile-only case. + +## Later native results from 2026-10-11 + +The ordinary SDK candidate first reached the new checks in `20261011T144953Z-1ce8`, exposing an incorrect +test assumption that a target selected before plugin activation must have a positive selection epoch. +Client legitimately starts at zero; the test now checks a nonnegative epoch and verifies that setting the +same pointer size preserves it. + +Run `20261011T145414Z-f9a6` then found a production bug: `process_list_threads` read `list.Strings[i]`, +but the installed CE reference documents `list[i]` (or `list.String[i]`). The previous double supplied +the unsupported property. The fix uses `list[i]`, and regression doubles expose only numeric entries. +All 637 standalone Lua tests passed after that fix; further adapter additions bring the final total to 651. + +Candidate 2 ran all eight added checks and six workflows successfully in `20261011T150242Z-86a0`, +then failed at the separate Plugin Manager bridge: its checklist remained empty. The earlier experiment +that retained the empty `Plugins64` key failed the same way. Installed CE 7.7 internals are not available; +we have no supported Lua workaround for host-driven disable/re-enable. The exact plugin-row guard stays intact. + +The dedicated `workflows` scenario passed in `20261011T151957Z-8cfc` in 33.5 seconds. It explicitly +stages all four optional gates disabled, checks the eight tools and six workflows, restarts target A, +restarts the gateway, then restarts host A while checking host B and rejecting stale identities. It records +zero plugin cycles. Its final summary reports `passed: true`, `userStateRestored: true`, +`sourceInstallationUnchanged: true` and an empty cleanup-failure list. No recorded owned processes remained. + +That private candidate uses SDK `2.0.1-mcpqualification.1` with the provider guards below. Its bundled DLL +and AOT gateway passed isolated catalog/load probes and gateway smoke checks. DLL SHA-256: +`F0DA5938E8CABE34C7BED9CCEE16FFE92C5731D9E37A9F2546835576DA594D68`; gateway SHA-256: +`E8547E93BF97BE9D0EFA07E225A27A4A6B41000454834CBF7B03A7FD10664C44`. +The private runner verified 930 input hashes before launch. This candidate is neither installed nor released. + +Its ordinary native bridge uses the checked-in SDK binary (`889DC4C2...`), whereas the diagnostic package +used the released CI-built binary (`B008C8D8...`). Their recorded native source/build fingerprints match; +the binaries are not byte-identical. PE and package audits passed. Successful private runs do not establish +that the original released-bundle dispatcher failure is resolved. + +## SDK investigation from 2026-10-11 + +The SDK source now refuses a provider state with a negative stack top before stamp verification or dispatch. +It also verifies that stamp checking preserves the stack top. Both failures release the operation admission +through the existing exception path; they do not reset the stack or retry the provider. These checks contain +an invalid state. They do not fix or explain the original corruption. + +Standalone SDK evidence: 278 native-Lua tests passed, 14 main-thread/coroutine tests passed, and the focused +provider/runtime run passed 14 tests. The rooted-coroutine fixture models the published CE 7.5 topology; +it does not establish the internals of the installed CE 7.7 host. + +The private diagnostic package `CheatEngine.SDK.2.0.1-mcpprovidertrace.2` was used in +`20261011T144032Z-1764`. The bounded resource/memory/scanner/AOB replay passed, restored user state, +left the installation unchanged and stopped all recorded owned processes. Host A recorded 3,593 trace +events and host B recorded 266. All 397 provider-returned states had nonnegative tops and stamp checking +preserved them. Neither trace froze, overflowed nor contained an incomplete record. The failure did not +reproduce; this is not root-cause resolution or full native qualification. + +An earlier candidate replay, `20261011T143815Z-b0c8`, also passed and restored state, but no trace was +created because the private harness had not forwarded the trace opt-in to its sanitized child environment. +That harness omission was corrected and reviewed before the recorded replay. Neither replay reached the +eight additional lifecycle checks. Raw traces, host logs and discovery material remain private. + +Client's dispatcher delegates to synchronous SDK invocation. No Client production change is supported by +the observed failure. Installed and released binaries remain unchanged. + +## Reproduce the evidence report + +Build and run the repository's portable and NativeLua categories as documented in +`tests/CheatEngine.Mcp.Tests/README.md`, with TRX reports enabled. Then run: + +```powershell +./eng/Write-AllToolQualificationReport.ps1 ` + -GatewayTrx artifacts/test-results/all-tools-20261016/portable-reviewed/CheatEngine.Mcp.Tests_net10.0_x64.trx ` + -PortableTrx artifacts/test-results/all-tools-20261016/portable-reviewed/CheatEngine.Mcp.Tests_net10.0_x64.trx ` + -NativeLuaTrx artifacts/test-results/all-tools-20261016/native-reviewed/CheatEngine.Mcp.Tests_net10.0_x64.trx ` + -OutputDirectory artifacts/test-results/all-tools-20261016/tool-report-final +``` + +The output is `tool-results.json` inside that output directory. Supply `-LiveSummary` with the private +session's `summary.json` to include its outcome. A receipt from a failed session is marked +`observed_in_failed_session`, never `verified`. Missing or duplicate routing cases leave their row +incomplete. `nativeQualificationComplete` stays false until the full per-tool native acceptance matrix +is implemented and reviewed; this report does not infer qualification from suite counts. diff --git a/docs/qualification/all-tools/behavior-evidence.json b/docs/qualification/all-tools/behavior-evidence.json new file mode 100644 index 0000000..e95cedc --- /dev/null +++ b/docs/qualification/all-tools/behavior-evidence.json @@ -0,0 +1,3189 @@ +{ + "schema": "cheatengine-mcp-behavior-evidence/v1", + "boundary": "Reviewed existing success assertions are partial evidence. Fixed Lua bodies bypass the adapter and do not qualify it. No row establishes native CE success.", + "tools": [ + { + "name": "aob_find", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.AobFind_IncludeMapped_OverrideIsOnDuringEveryPatternScanAndEndsOnce", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaAobMappedTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([\"401000\"], result.Results[0].Matches); Assert.Equal(new object?[] { true, true }, duringScans);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.AobFind_IncludeMapped_OverrideIsOnDuringEveryPatternScanAndEndsOnce" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "aob_find_value", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Aob/AobToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(pattern, request!.Value.Pattern.Value); Assert.Equal([\"401000\"], result.Result.Matches); Assert.Equal(1, target.Dispatcher.Calls);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment(valueType: Double, value: \"1\", pattern: \"00 00 00 00 00 00 F0 3F\", alignment: 4)", + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment(valueType: Float, value: \"1.5\", pattern: \"00 00 C0 3F\", alignment: 4)", + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment(valueType: Int16, value: \"-2\", pattern: \"FE FF\", alignment: 2)", + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment(valueType: Int32, value: \"100\", pattern: \"64 00 00 00\", alignment: 4)", + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment(valueType: Int64, value: \"1\", pattern: \"01 00 00 00 00 00 00 00\", alignment: 4)", + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment(valueType: Int8, value: \"-1\", pattern: \"FF\", alignment: 1)", + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment(valueType: String, value: \"Hi\", pattern: \"48 69\", alignment: 1)", + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment(valueType: UInt32, value: \"0xDEADBEEF\", pattern: \"EF BE AD DE\", alignment: 4)", + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment(valueType: UInt8, value: \"0x7F\", pattern: \"7F\", alignment: 1)", + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.FindValue_Value_IsEncodedAsTheTargetBytesWithItsDefaultAlignment(valueType: WString, value: \"Hi\", pattern: \"48 00 69 00\", alignment: 1)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "aob_generate_signature", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.GenerateSignature_UniquePattern_NormalizesWildcardsAndVerifiesTheMatchStart", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Aob/AobToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new AobSignature(\"401002\", \"game.exe\", AobSignatureGenerator.CheatEngine, true, true, \"48 8B ?? 05\", \"401000\", 2, 4, 1), signature);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Aob.AobToolsTests.GenerateSignature_UniquePattern_NormalizesWildcardsAndVerifiesTheMatchStart" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "asm_apply", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ApplyAndCodePatch_RetainExactLeaseMetadataAndReleaseOriginalDisable", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs", + "scope": "adapter", + "oracle": "Exact supplied/generated ENABLE and DISABLE bytes; retained lease metadata, one release, empty patch/resource lists afterward.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ApplyAndCodePatch_RetainExactLeaseMetadataAndReleaseOriginalDisable(codePatch: False)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "asm_apply_code_patch", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ApplyAndCodePatch_RetainExactLeaseMetadataAndReleaseOriginalDisable", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs", + "scope": "adapter", + "oracle": "Exact supplied/generated ENABLE and DISABLE bytes; retained lease metadata, one release, empty patch/resource lists afterward.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ApplyAndCodePatch_RetainExactLeaseMetadataAndReleaseOriginalDisable(codePatch: True)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "asm_assemble", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.Assemble_SequentialInstructions_ForwardActualOriginsAndEncodingOptions", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "8BC3C3 bytes, lengths and source order, origins 401000 then 401002, exact encoding/range-check requests.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.Assemble_SequentialInstructions_ForwardActualOriginsAndEncodingOptions" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "asm_check", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.Check_AcceptedEnable_AlsoChecksTheDisableSection", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new AsmCheckResult(false, \"Error in line 4 (bad) :This instruction can't be compiled\", false, AsmScriptSection.Disable), result);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.Check_AcceptedEnable_AlsoChecksTheDisableSection" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "asm_generate_api_hook", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.AsmV2_GenerateApiHook_JoinsCheatEnginesTwoSectionsAndPreservesEveryArgument", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaAsmDebuggerV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(expectedScript, generated.Script); Assert.Equal((\"401000\", \"401005\", \"newCall\", \"x64\", true), observed globals);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.AsmV2_GenerateApiHook_JoinsCheatEnginesTwoSectionsAndPreservesEveryArgument" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "asm_generate_injection", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.GenerateInjection_ProducesReviewedRollbackScript", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((\"48895C2408\", \"mcpHook\"), (result.ExpectedBytes, result.SymbolName)); Assert.Contains(\"[DISABLE]\", result.Script);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.GenerateInjection_ProducesReviewedRollbackScript" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "asm_list_patches", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ApplyAndCodePatch_RetainExactLeaseMetadataAndReleaseOriginalDisable", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs", + "scope": "adapter", + "oracle": "Exact supplied/generated ENABLE and DISABLE bytes; retained lease metadata, one release, empty patch/resource lists afterward.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ApplyAndCodePatch_RetainExactLeaseMetadataAndReleaseOriginalDisable(codePatch: False)", + "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ApplyAndCodePatch_RetainExactLeaseMetadataAndReleaseOriginalDisable(codePatch: True)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "asm_release_patch", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ReleasePatch_CompletedLease_ForgetsTheRetainedPatchAndResource", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.True(stopped.Released); Assert.Empty(harness.Resources.List());", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ReleasePatch_CompletedLease_ForgetsTheRetainedPatchAndResource" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_clear_dissect", + "cases": [ + { + "suite": "portable", + "scope": "adapter", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectorViews_MapFixedLuaPagesAndClearSideEffect", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "oracle": "Known reference, string, function-page and clear result fields are asserted; the four fixed-Lua operation names occur in the requested order.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectorViews_MapFixedLuaPagesAndClearSideEffect" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_decode", + "cases": [ + { + "suite": "portable", + "scope": "adapter", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DecodeAndDisassemble_MapTypedInstructionsInRequestedOrder", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "oracle": "Known typed or fixed-Lua snapshots are asserted against the returned tool result; exact forwarded requests and operation order are also checked.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DecodeAndDisassemble_MapTypedInstructionsInRequestedOrder" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_disassemble", + "cases": [ + { + "suite": "portable", + "scope": "adapter", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DecodeAndDisassemble_MapTypedInstructionsInRequestedOrder", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "oracle": "Known typed or fixed-Lua snapshots are asserted against the returned tool result; exact forwarded requests and operation order are also checked.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DecodeAndDisassemble_MapTypedInstructionsInRequestedOrder" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_disassemble_bytes", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.CodeDisassembleBytes_SpacedText_ReachesDisassembleBytesAsContiguousDigits", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaCodeTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new CodeByteDisassembly(\"1000\", \"mov rax,[00001007]\"), result); Assert.Equal((\"488B0500000000\", 0x1000L), (ReadGlobal(\"receivedBytes\"), ReadGlobal(\"receivedOrigin\")));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.CodeDisassembleBytes_SpacedText_ReachesDisassembleBytesAsContiguousDigits" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_find_references", + "cases": [ + { + "suite": "portable", + "scope": "adapter", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectorViews_MapFixedLuaPagesAndClearSideEffect", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "oracle": "Known reference, string, function-page and clear result fields are asserted; the four fixed-Lua operation names occur in the requested order.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectorViews_MapFixedLuaPagesAndClearSideEffect" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_find_strings", + "cases": [ + { + "suite": "portable", + "scope": "adapter", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectorViews_MapFixedLuaPagesAndClearSideEffect", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "oracle": "Known reference, string, function-page and clear result fields are asserted; the four fixed-Lua operation names occur in the requested order.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectorViews_MapFixedLuaPagesAndClearSideEffect" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_get_comments", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.CodeComments_StubbedApi_KeepsReadOrderAndWritesTheRequestedComment", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaCodeQualificationTests.cs", + "scope": "fixed_lua_body", + "oracle": "Assert.Equal([new CodeComment(\"401000\", \"entry\"), new CodeComment(\"402000\", null)], read.Comments);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.CodeComments_StubbedApi_KeepsReadOrderAndWritesTheRequestedComment" + ] + }, + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.CodeComments_ForwardOrderedExpressionsAndReturnTheFixedLuaValues", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeTableQualificationTests.cs", + "scope": "adapter", + "oracle": "Ordered arguments and all comment result fields", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.CodeComments_ForwardOrderedExpressionsAndReturnTheFixedLuaValues" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_get_function", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.CodeGetFunction_StubbedApi_ReturnsBoundariesAndJumpDestination", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaCodeQualificationTests.cs", + "scope": "fixed_lua_body", + "oracle": "Assert.Equal(new CodeLuaFunction(true, \"401020\", \"401000\", \"401080\", 128, true), result);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.CodeGetFunction_StubbedApi_ReturnsBoundariesAndJumpDestination" + ] + }, + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.CodeGetFunction_MapsKnownBoundariesAndJumpDestination", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeTableQualificationTests.cs", + "scope": "adapter", + "oracle": "Independent boundaries, size and jump destination", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.CodeGetFunction_MapsKnownBoundariesAndJumpDestination" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_get_function_graph", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Code.CodeFunctionGraphTests.GetFunctionGraph_LoopBranchAndCalls_SplitsBlocksAtEveryLeader", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Code/CodeFunctionGraphTests.cs", + "scope": "adapter", + "oracle": "Exact five block boundaries/terminators/successors, direct and indirect calls, ten instructions and symbol lookup.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Code.CodeFunctionGraphTests.GetFunctionGraph_LoopBranchAndCalls_SplitsBlocksAtEveryLeader" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_list_functions", + "cases": [ + { + "suite": "portable", + "scope": "adapter", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectorViews_MapFixedLuaPagesAndClearSideEffect", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "oracle": "Known reference, string, function-page and clear result fields are asserted; the four fixed-Lua operation names occur in the requested order.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectorViews_MapFixedLuaPagesAndClearSideEffect" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_poll_job", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectJob_KnownRange_PollsRetainedEventAndReleasesJob", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known 16-byte range and completed progress, one retained dissect event, cursor/repeat-poll semantics, stopped job and empty resources.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectJob_KnownRange_PollsRetainedEventAndReleasesJob" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_set_comment", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.CodeComments_StubbedApi_KeepsReadOrderAndWritesTheRequestedComment", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaCodeQualificationTests.cs", + "scope": "fixed_lua_body", + "oracle": "Assert.Equal(new CodeComment(\"403000\", \"updated\"), written); Assert.Equal((0x403000L, \"updated\"), globals);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.CodeComments_StubbedApi_KeepsReadOrderAndWritesTheRequestedComment" + ] + }, + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.CodeSetComment_ForwardsExactAddressAndBlankCommentToTheFixedBody", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeTableQualificationTests.cs", + "scope": "adapter", + "oracle": "Exact address/blank comment and returned fields", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.CodeSetComment_ForwardsExactAddressAndBlankCommentToTheFixedBody" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_start_dissect", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectJob_KnownRange_PollsRetainedEventAndReleasesJob", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known 16-byte range and completed progress, one retained dissect event, cursor/repeat-poll semantics, stopped job and empty resources.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.DissectJob_KnownRange_PollsRetainedEventAndReleasesJob" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "code_start_search", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.StartSearch_StopWhileNativeCallRuns_ReportsPartialThenReleasesTheJob", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.True(stopped.AlreadyReleased); Assert.Equal(JobState.Stopped, job.State);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.StartSearch_StopWhileNativeCallRuns_ReportsPartialThenReleasesTheJob" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_attach", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/status/context/register/thread/stack results and exact continuation/control arguments through inert CE APIs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_break_thread", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/status/context/register/thread/stack results and exact continuation/control arguments through inert CE APIs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_continue", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/status/context/register/thread/stack results and exact continuation/control arguments through inert CE APIs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_delete_breakpoint", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_BreakpointLifecycle_TracksThenReleasesTheKnownOwnedBreakpoint", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known breakpoint attributes, normalized resolved address, tracked resource and deletion by owned CE breakpoint ID.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_BreakpointLifecycle_TracksThenReleasesTheKnownOwnedBreakpoint" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_detach", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Debugger.DebuggerToolsTests.Detach_JobCompletedInLua_NoLongerBlocks", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Debugger/DebuggerToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.True(detached.Detached); Assert.Equal(TargetResourceState.Ended, Assert.Single(harness.Resources.List()).State);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Debugger.DebuggerToolsTests.Detach_JobCompletedInLua_NoLongerBlocks" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_get_context", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/status/context/register/thread/stack results and exact continuation/control arguments through inert CE APIs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_get_stack_trace", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/status/context/register/thread/stack results and exact continuation/control arguments through inert CE APIs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_get_status", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/status/context/register/thread/stack results and exact continuation/control arguments through inert CE APIs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_list_breakpoints", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Debugger.PreparedBreakpointNavigationTests.ExplicitList_PreparesMaxCopyAndProjectsRequestedAndDefaultLimits", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Debugger/PreparedBreakpointNavigationTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((2, 3, true), (explicitPage.Breakpoints.Length, explicitPage.Total, explicitPage.Truncated));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Debugger.PreparedBreakpointNavigationTests.ExplicitList_PreparesMaxCopyAndProjectsRequestedAndDefaultLimits" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_poll_capture", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Debugger.DebuggerToolsTests.Capture_PollThenStop_RetainsTypedHitsAndReleasesTheLuaJob", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Debugger/DebuggerToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((\"401010\", 17L, \"1\"), (hit.Context.Ip, hit.Context.ThreadId, hit.Context.Registers[\"RAX\"])); Assert.Equal((started.JobId, JobState.Running, 1L, 1L, false, 0L), (...));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Debugger.DebuggerToolsTests.Capture_PollThenStop_RetainsTypedHitsAndReleasesTheLuaJob" + ] + }, + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known target/address/TTL, capture hit at 401000, trace RAX=1, run-to continuation and ledger/job cleanup.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_poll_trace", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known target/address/TTL, capture hit at 401000, trace RAX=1, run-to continuation and ledger/job cleanup.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_run_to", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known target/address/TTL, capture hit at 401000, trace RAX=1, run-to continuation and ledger/job cleanup.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_set_breakpoint", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_BreakpointLifecycle_TracksThenReleasesTheKnownOwnedBreakpoint", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known breakpoint attributes, normalized resolved address, tracked resource and deletion by owned CE breakpoint ID.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_BreakpointLifecycle_TracksThenReleasesTheKnownOwnedBreakpoint" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_set_register", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/status/context/register/thread/stack results and exact continuation/control arguments through inert CE APIs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_set_thread_ignored", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/status/context/register/thread/stack results and exact continuation/control arguments through inert CE APIs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_start_capture", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known target/address/TTL, capture hit at 401000, trace RAX=1, run-to continuation and ledger/job cleanup.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_start_trace", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known target/address/TTL, capture hit at 401000, trace RAX=1, run-to continuation and ledger/job cleanup.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "debugger_step", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/status/context/register/thread/stack results and exact continuation/control arguments through inert CE APIs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "dotnet_get_method_parameters", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetParameterAndObjectAdapters_MapKnownFieldsAndNormalizedInputs", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known parameter type-name conversion and object field value/offset, with normalized inputs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetParameterAndObjectAdapters_MapKnownFieldsAndNormalizedInputs" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "dotnet_get_object", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetParameterAndObjectAdapters_MapKnownFieldsAndNormalizedInputs", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known parameter type-name conversion and object field value/offset, with normalized inputs.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetParameterAndObjectAdapters_MapKnownFieldsAndNormalizedInputs" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "dotnet_get_status", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetDiscoveryAdapters_MapStatusDomainsAndModulesWithRequestedPage", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/domain/module fields, totals and exact normalized page/handle arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetDiscoveryAdapters_MapStatusDomainsAndModulesWithRequestedPage" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "dotnet_get_type", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetTypeAdapters_MapPagesDetailsAndMethodsWithFilters", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known type/base/field/method values and normalized handle, page and filter arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetTypeAdapters_MapPagesDetailsAndMethodsWithFilters" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "dotnet_list_domains", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetDiscoveryAdapters_MapStatusDomainsAndModulesWithRequestedPage", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/domain/module fields, totals and exact normalized page/handle arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetDiscoveryAdapters_MapStatusDomainsAndModulesWithRequestedPage" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "dotnet_list_methods", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetTypeAdapters_MapPagesDetailsAndMethodsWithFilters", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known type/base/field/method values and normalized handle, page and filter arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetTypeAdapters_MapPagesDetailsAndMethodsWithFilters" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "dotnet_list_modules", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetDiscoveryAdapters_MapStatusDomainsAndModulesWithRequestedPage", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/domain/module fields, totals and exact normalized page/handle arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetDiscoveryAdapters_MapStatusDomainsAndModulesWithRequestedPage" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "dotnet_list_types", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetTypeAdapters_MapPagesDetailsAndMethodsWithFilters", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known type/base/field/method values and normalized handle, page and filter arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.DotNetTypeAdapters_MapPagesDetailsAndMethodsWithFilters" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "dotnet_poll_instance_search", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.DotNet.DotNetToolsTests.InstanceSearch_StartPollAndStop_RetainsTheBoundedResultThenDiscardsIt", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/DotNet/DotNetToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([new DotNetInstance(\"1000\"), new DotNetInstance(\"2000\")], page.Instances); Assert.Equal((1L, 2L, false, 0L), (...)); Assert.Equal((0, 0), (harness.Jobs.Count, harness.Resources.Count));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.DotNet.DotNetToolsTests.InstanceSearch_StartPollAndStop_RetainsTheBoundedResultThenDiscardsIt" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "dotnet_start_instance_search", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.DotNet.DotNetToolsTests.InstanceSearch_StartPollAndStop_RetainsTheBoundedResultThenDiscardsIt", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/DotNet/DotNetToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.StartsWith($\"dotnetinstances-{harness.Jobs.Namespace}-\", started.JobId, StringComparison.Ordinal); Assert.Equal((\"module\", \"token\", 2), (started.ModuleHandle, started.TypeToken, started.MaximumResults));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.DotNet.DotNetToolsTests.InstanceSearch_StartPollAndStop_RetainsTheBoundedResultThenDiscardsIt" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "exec_call_local", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_CallLocal_EnablesTheFeatureAndForwardsTheKnownResult", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_CallLocal_EnablesTheFeatureAndForwardsTheKnownResult" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "exec_call_method", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_CallMethod_ResolvesMethodAndInstanceBeforeCalling", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_CallMethod_ResolvesMethodAndInstanceBeforeCalling" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "exec_call_remote", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_CallRemote_ResolvesTheFunctionAndForwardsTypedArguments", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_CallRemote_ResolvesTheFunctionAndForwardsTypedArguments" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "exec_compile_c", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_CompileC_TargetSelfForwardsFlagsAndSortsTheKnownSymbols", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_CompileC_TargetSelfForwardsFlagsAndSortsTheKnownSymbols" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "exec_compile_csharp", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_CompileCSharp_ExportsTheNativeCompilerArtifact", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_CompileCSharp_ExportsTheNativeCompilerArtifact" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "exec_inject_dotnet", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_InjectDotNet_HoldsTheApprovedAssemblyAndForwardsEveryArgument", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_InjectDotNet_HoldsTheApprovedAssemblyAndForwardsEveryArgument" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "exec_inject_library", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_InjectLibrary_HoldsTheApprovedArtifactAndForwardsTheWaitFlag", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ExecutionAdapter_InjectLibrary_HoldsTheApprovedArtifactAndForwardsTheWaitFlag" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "kernel_get_status", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.KernelAdapter_GetStatus_EnablesKernelFeatureAndCopiesKnownState", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.KernelAdapter_GetStatus_EnablesKernelFeatureAndCopiesKnownState" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "kernel_initialize_dbvm", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Kernel.KernelToolsTests.InitializeDbvm_PassesCheatEnginesWarningAndTheReasonSeparately", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Kernel/KernelToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new KernelDbvmInitialization(true, offload, \"Watch a page\"), result); Assert.Contains(\"Watch a page\", source);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Kernel.KernelToolsTests.InitializeDbvm_PassesCheatEnginesWarningAndTheReasonSeparately(offload: False)", + "CheatEngine.Mcp.Tests.Tools.Kernel.KernelToolsTests.InitializeDbvm_PassesCheatEnginesWarningAndTheReasonSeparately(offload: True)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "kernel_poll_watch", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Kernel.KernelToolsTests.PollWatch_DrainsDbvmThenReadsThePageThroughTheJobPoll", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Kernel/KernelToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([1L, 2L], page.Events.Select(static item => item.SourceIndex)); Assert.Equal((\"401000\", \"1\", \"1AB000\"), (page.Events[0].Rip, page.Events[0].Rax, page.Events[0].Cr3));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Kernel.KernelToolsTests.PollWatch_DrainsDbvmThenReadsThePageThroughTheJobPoll" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "kernel_read_physical", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.KernelAdapter_ReadPhysical_EnablesKernelFeatureAndReturnsTheRequestedByteRange", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.KernelAdapter_ReadPhysical_EnablesKernelFeatureAndReturnsTheRequestedByteRange" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "kernel_start_watch", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Kernel.KernelToolsTests.StartWatch_RangeThatEndsAtThePageBoundary_IsArmed", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Kernel/KernelToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((physicalAddress, byteSize), (started.PhysicalAddress, started.ByteSize));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Kernel.KernelToolsTests.StartWatch_RangeThatEndsAtThePageBoundary_IsArmed(physicalAddress: \"2000\", byteSize: 4096)", + "CheatEngine.Mcp.Tests.Tools.Kernel.KernelToolsTests.StartWatch_RangeThatEndsAtThePageBoundary_IsArmed(physicalAddress: \"2800\", byteSize: 2048)", + "CheatEngine.Mcp.Tests.Tools.Kernel.KernelToolsTests.StartWatch_RangeThatEndsAtThePageBoundary_IsArmed(physicalAddress: \"2FFF\", byteSize: 1)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "kernel_translate_address", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.KernelAdapter_TranslateAddress_ResolvesTheVirtualAddressBeforeTheDbkCall", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.KernelAdapter_TranslateAddress_ResolvesTheVirtualAddressBeforeTheDbkCall" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "kernel_write_physical", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.KernelAdapter_WritePhysical_EnablesKernelFeatureAndForwardsEachByte", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Actual adapter through standalone Lua with inert CE API doubles: known result and forwarded arguments; file-backed cases also check held-file release.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.KernelAdapter_WritePhysical_EnablesKernelFeatureAndForwardsEachByte" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "lua_execute", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Lua.LuaToolQualificationTests.Execute_Success_UsesTheUnsafeAdapterAndKeepsCopiedValuesAndOpaqueCount", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Lua/LuaToolQualificationTests.cs", + "scope": "adapter", + "oracle": "Unsafe adapter result values and opaque count", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Lua.LuaToolQualificationTests.Execute_Success_UsesTheUnsafeAdapterAndKeepsCopiedValuesAndOpaqueCount" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "lua_find_api", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Lua.LuaToolQualificationTests.FindApi_ForwardsExactPageArgumentsAndReturnsEveryKnownPageField", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Lua/LuaToolQualificationTests.cs", + "scope": "adapter", + "oracle": "Exact query/page arguments and complete page result", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Lua.LuaToolQualificationTests.FindApi_ForwardsExactPageArgumentsAndReturnsEveryKnownPageField" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_allocate", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryAllocationToolsTests.Allocate_Executable_RequestsExecuteReadWriteAndTracksTheLease", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryAllocationToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((\"cave\", \"7FF000010000\", 64L, true), (info.Name, info.Address, info.Size, info.Executable));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryAllocationToolsTests.Allocate_Executable_RequestsExecuteReadWriteAndTracksTheLease" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_compare", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Compare_Differences_ListsRunsAndStopsAtTheLimit", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryReadToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([new MemoryDifference(\"1\", 2, \"02 03\", \"09 09\"), new MemoryDifference(\"6\", 1, \"07\", \"00\")], all.Differences);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Compare_Differences_ListsRunsAndStopsAtTheLimit" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_compare_snapshot", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemorySnapshotToolsTests.CompareSnapshot_TwoSnapshots_ListsChangedSlotsWithoutAnyDispatch", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemorySnapshotToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([new SnapshotChange(\"10004\", \"4\", \"0\", \"2\"), new SnapshotChange(\"1000C\", \"C\", \"0\", \"255\")], diff.Changes);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemorySnapshotToolsTests.CompareSnapshot_TwoSnapshots_ListsChangedSlotsWithoutAnyDispatch" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_copy", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryWriteToolsTests.Copy_Range_ReadsTheWholeSourceBeforeWritingTheDestination", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryWriteToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new MemoryCopyResult(\"1000\", \"2001\", 3), result);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryWriteToolsTests.Copy_Range_ReadsTheWholeSourceBeforeWritingTheDestination" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_create_snapshot", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemorySnapshotToolsTests.CreateSnapshot_ReadableRange_CopiesTheBytesAndDescribesTheTarget", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemorySnapshotToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(memory, store.Get(\"before\", \"name\").Image.Bytes);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemorySnapshotToolsTests.CreateSnapshot_ReadableRange_CopiesTheBytesAndDescribesTheTarget" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_delete_snapshot", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemorySnapshotToolsTests.DeleteSnapshot_FreesItsBytesAndAnUnknownNameIsNotFound", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemorySnapshotToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new MemorySnapshotDeleted(\"gone\", 16), deleted);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemorySnapshotToolsTests.DeleteSnapshot_FreesItsBytesAndAnUnknownNameIsNotFound" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_dump_to_file", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryFileToolsTests.DumpToFile_ReadableRange_WritesTheBytesAndLeavesNoTemporaryFile", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryFileToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(memory, File.ReadAllBytes(path));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryFileToolsTests.DumpToFile_ReadableRange_WritesTheBytesAndLeavesNoTemporaryFile" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_free", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryAllocationToolsTests.Free_RetryableRelease_KeepsTheAllocationTrackedUntilAReleaseCompletes", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryAllocationToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(ResourceReleaseKind.Released, released.Release.Kind); Assert.Empty(harness.Resources.List());", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryAllocationToolsTests.Free_RetryableRelease_KeepsTheAllocationTrackedUntilAReleaseCompletes" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_get_address_info", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryInfoToolsTests.GetAddressInfo_ResolvedAndUnresolved_DescribesEachInOneDispatch", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryInfoToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((\"401010\", \"game.exe+1010\", \"game.exe\", \".text\", (bool?) false), (player.Address, player.Symbol, player.Module, player.Section, player.IsSystemModule));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryInfoToolsTests.GetAddressInfo_ResolvedAndUnresolved_DescribesEachInOneDispatch" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_hash", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Hash_ChunkedRange_MatchesTheManagedDigest", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryReadToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(Convert.ToHexStringLower(SHA256.HashData(memory)), sha256.Hash);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Hash_ChunkedRange_MatchesTheManagedDigest" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_list_regions", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryInfoToolsTests.ListRegions_Filters_KeepMatchingRegionsInAddressOrderAndPage", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryInfoToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([\"10000\", \"401000\", \"402000\", \"500000\"], all.Regions.Select(static region => region.Base));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryInfoToolsTests.ListRegions_Filters_KeepMatchingRegionsInAddressOrderAndPage" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_list_snapshots", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemorySnapshotToolsTests.ListSnapshots_OldestFirst_ReportsTheBytesWithoutAnyDispatch", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemorySnapshotToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([\"one\", \"two\"], list.Snapshots.Select(static snapshot => snapshot.Name));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemorySnapshotToolsTests.ListSnapshots_OldestFirst_ReportsTheBytesWithoutAnyDispatch" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_load_from_file", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryFileToolsTests.LoadFromFile_FileLargerThanOneChunk_WritesContiguousBoundedBlocks", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryFileToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new FileLoadResult(path, \"1000\", input.Length), result);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryFileToolsTests.LoadFromFile_FileLargerThanOneChunk_WritesContiguousBoundedBlocks" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_read", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryReadToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new MemoryReadResult(\"401010\", type, expected), result);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: Double, expected: \"-0.25\")", + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: Float, expected: \"1.5\")", + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: Int16, expected: \"-300\")", + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: Int32, expected: \"1337\")", + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: Int64, expected: \"-9000000000\")", + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: Int8, expected: \"-5\")", + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: Pointer, expected: \"7FF6A1B2C3D0\")", + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: UInt16, expected: \"60000\")", + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: UInt32, expected: \"4000000000\")", + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: UInt64, expected: \"18000000000000000000\")", + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.Read_FixedType_RoutesToTheTypedPrimitiveOfItsWidth(type: UInt8, expected: \"200\")" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_read_batch", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.ReadBatch_MixedItems_ReadsEachFixedTypeInOneTypedBatchAndKeepsOrder", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryReadToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([\"1\", \"0.5\", \"90 C3\", \"2\", null], result.Items.Select(static item => item.Value));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryReadToolsTests.ReadBatch_MixedItems_ReadsEachFixedTypeInOneTypedBatchAndKeepsOrder" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_read_samples", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemorySampleToolsTests.ReadSamples_ValueChanges_KeepTheFirstValueAndEveryChangeWithItsTime", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemorySampleToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([new MemorySamplePoint(0, \"5\"), new MemorySamplePoint(200, \"6\"), new MemorySamplePoint(400, \"5\")], first.Changes);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemorySampleToolsTests.ReadSamples_ValueChanges_KeepTheFirstValueAndEveryChangeWithItsTime" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_set_protection", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryWriteToolsTests.SetProtection_FixedScript_ReportsPreviousAndCurrentAccess", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryWriteToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new ProtectionChange(\"401000\", 4096, new ProtectionFlags(true, false, true), new ProtectionFlags(true, true, true)), change);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryWriteToolsTests.SetProtection_FixedScript_ReportsPreviousAndCurrentAccess" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_write", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryWriteToolsTests.Write_Int32_PreReadsPreviousWritesTypedAndVerifiesTheReadBack", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryWriteToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new MemoryWriteResult(\"1000\", 4, \"64 00 00 00\", true), result);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryWriteToolsTests.Write_Int32_PreReadsPreviousWritesTypedAndVerifiesTheReadBack" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "memory_write_batch", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Memory.MemoryWriteToolsTests.WriteBatch_ConsecutiveTypes_WriteInRequestOrderAndVerify", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Memory/MemoryWriteToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((5, false), (result.Written, result.Verified)); Assert.Equal([2], result.Mismatched!);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Memory.MemoryWriteToolsTests.WriteBatch_ConsecutiveTypes_WriteInRequestOrderAndVerify" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "module_find_patches", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Modules.ModulePatchToolTests.FindPatches_HookedCode_ReportsTheRangesAfterApplyingRelocations", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Modules/ModulePatchToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new ModulePatch(\"7FFA00004040\", \".text\", 5, \"91 92 93 94 95\", \"E9 11 22 33 44\", \"sample.Alpha+30\"), patch);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Modules.ModulePatchToolTests.FindPatches_HookedCode_ReportsTheRangesAfterApplyingRelocations" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "module_get", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Modules.ModuleToolTests.Get_Module_ReportsSectionsWithTheirFlagsThePeFieldsAndThePdb", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Modules/ModuleToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"game.exe\", result.Name);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Modules.ModuleToolTests.Get_Module_ReportsSectionsWithTheirFlagsThePeFieldsAndThePdb" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "module_list", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Modules.ModuleToolTests.List_AttachedProcess_PagesTheFilteredModulesInBothFormats", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Modules/ModuleToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(2, result.Total);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Modules.ModuleToolTests.List_AttachedProcess_PagesTheFilteredModulesInBothFormats" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "module_list_exports", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Modules.ModuleToolTests.ListExports_Module_PagesExportsByOrdinalWithForwarders", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Modules/ModuleToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(2, result.Total);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Modules.ModuleToolTests.ListExports_Module_PagesExportsByOrdinalWithForwarders" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "module_list_imports", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Modules.ModuleImportToolTests.ListImports_Module_ListsEverySlotWithItsValueAndTarget", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Modules/ModuleImportToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((\"sample.dll\", 6, (int?) null), (list.Module, list.Total, list.NextOffset)); Assert.Equal(expected, list.Imports); Assert.Equal(1, target.Dispatcher.Calls);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Modules.ModuleImportToolTests.ListImports_Module_ListsEverySlotWithItsValueAndTarget" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_attach", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoGatedMutationAdapters_DeclareTheGateMapResultsAndReleaseAttachment", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/JIT/invocation values, exact arguments and detach/resource cleanup.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoGatedMutationAdapters_DeclareTheGateMapResultsAndReleaseAttachment" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_compile_method", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoGatedMutationAdapters_DeclareTheGateMapResultsAndReleaseAttachment", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/JIT/invocation values, exact arguments and detach/resource cleanup.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoGatedMutationAdapters_DeclareTheGateMapResultsAndReleaseAttachment" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_detach", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Mono.MonoToolsTests.AttachThenDetach_ReleasesTheActivationOwnedCollectorResource", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Mono/MonoToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(TargetResourceState.Ended, Assert.Single(harness.Resources.List()).State);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Mono.MonoToolsTests.AttachThenDetach_ReleasesTheActivationOwnedCollectorResource" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_find_class", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoLookupAdapters_MapClassMethodAndStaticAddressWithExactHandles", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known class/method/static address and exact normalized lookup handles/names.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoLookupAdapters_MapClassMethodAndStaticAddressWithExactHandles" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_find_method", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoLookupAdapters_MapClassMethodAndStaticAddressWithExactHandles", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known class/method/static address and exact normalized lookup handles/names.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoLookupAdapters_MapClassMethodAndStaticAddressWithExactHandles" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_get_object", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Mono.MonoToolsTests.GetObject_TrimmedAddressAndBounds_ReachTheObjectBodyInOneDispatch", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Mono/MonoToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(expected, result); Assert.Equal(1, harness.Dispatches);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Mono.MonoToolsTests.GetObject_TrimmedAddressAndBounds_ReachTheObjectBodyInOneDispatch(maxBacktrack: 0, maximumFields: 1)", + "CheatEngine.Mcp.Tests.Tools.Mono.MonoToolsTests.GetObject_TrimmedAddressAndBounds_ReachTheObjectBodyInOneDispatch(maxBacktrack: 65536, maximumFields: 2048)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_get_static_field_address", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoLookupAdapters_MapClassMethodAndStaticAddressWithExactHandles", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known class/method/static address and exact normalized lookup handles/names.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoLookupAdapters_MapClassMethodAndStaticAddressWithExactHandles" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_get_status", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known collector version, assembly/image/class/field/method values, totals and exact page/filter arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_invoke_method", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoGatedMutationAdapters_DeclareTheGateMapResultsAndReleaseAttachment", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known attachment/JIT/invocation values, exact arguments and detach/resource cleanup.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoGatedMutationAdapters_DeclareTheGateMapResultsAndReleaseAttachment" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_list_assemblies", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known collector version, assembly/image/class/field/method values, totals and exact page/filter arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_list_classes", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known collector version, assembly/image/class/field/method values, totals and exact page/filter arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_list_fields", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known collector version, assembly/image/class/field/method values, totals and exact page/filter arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_list_methods", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs", + "scope": "adapter", + "oracle": "Known collector version, assembly/image/class/field/method values, totals and exact page/filter arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.ManagedAdapterQualificationTests.MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_poll_instance_search", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Mono.MonoToolsTests.InstanceSearch_StartPollAndStop_RetainsTheBoundedResultThenDiscardsIt", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Mono/MonoToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([new MonoInstance(\"3000\"), new MonoInstance(\"4000\")], page.Instances); Assert.Equal((0, 0), (harness.Jobs.Count, harness.Resources.Count));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Mono.MonoToolsTests.InstanceSearch_StartPollAndStop_RetainsTheBoundedResultThenDiscardsIt" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "mono_start_instance_search", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Mono.MonoToolsTests.InstanceSearch_StartPollAndStop_RetainsTheBoundedResultThenDiscardsIt", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Mono/MonoToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.StartsWith($\"monoinstances-{harness.Jobs.Namespace}-\", started.JobId, StringComparison.Ordinal); Assert.Equal((\"class\", 2), (started.ClassHandle, started.MaximumResults));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Mono.MonoToolsTests.InstanceSearch_StartPollAndStop_RetainsTheBoundedResultThenDiscardsIt" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_create_map", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.CreateMapThenFindPaths_Jobs_StoreRankedModuleRootedPaths", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerJobToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((PointerJobState.Ready, PointerFixture.ProcessId, 8, 3, false, 100), (map.State, map.ProcessId, map.PointerSize, map.Pointers, map.Incomplete, map.ProgressPercent));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.CreateMapThenFindPaths_Jobs_StoreRankedModuleRootedPaths" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_delete_map", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.DeleteMap_RunningCapture_StopsItsJobAndForgetsTheMap", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerJobToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new PointerDeleteResult(\"map\", true), deleted); Assert.Empty(fixture.Maps.ListMaps().Maps);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.DeleteMap_RunningCapture_StopsItsJobAndForgetsTheMap" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_delete_scan", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.DeleteScanAndMap_EndedJobs_AreNotCancelledAndFreeTheStore", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerJobToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new PointerDeleteResult(\"hp\", false), fixture.Scans.DeleteScan(\"hp\", Token)); Assert.Empty(fixture.Scans.ListScans().Scans);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.DeleteScanAndMap_EndedJobs_AreNotCancelledAndFreeTheStore" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_find_paths", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.CreateMapThenFindPaths_Jobs_StoreRankedModuleRootedPaths", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerJobToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((PointerJobState.Ready, 2, false, false, \"21020\"), (searched.State, searched.Count, searched.Incomplete, searched.TraversalLimited, searched.Target));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.CreateMapThenFindPaths_Jobs_StoreRankedModuleRootedPaths" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_find_references", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerReferenceToolTests.FindReferences_LiveExact_ScansTheAlignedLittleEndianPointerInWritableMemory", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerReferenceToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new PointerReference(\"10100\", \"20000\", \"0\", \"game.exe+100\"), reference);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerReferenceToolTests.FindReferences_LiveExact_ScansTheAlignedLittleEndianPointerInWritableMemory" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_get_access_info", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.PointerAccess_PaddedHexFacts_NormalizesMapsAndObservedAddress", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaPointerAccessTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(PointerAccessStatus.Supported, result.Status); Assert.Equal(\"1010\", result.EffectiveAddress);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.PointerAccess_PaddedHexFacts_NormalizesMapsAndObservedAddress(instruction: \"mov eax,[game.exe+10]\")", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.PointerAccess_PaddedHexFacts_NormalizesMapsAndObservedAddress(instruction: \"mov eax,[rax+10]\")" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_list_maps", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.ListTools_ThroughTheServer_ReturnStructuredMapsAndScans", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerJobToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((\"map\", \"ready\"), (map.GetProperty(\"mapName\").GetString(), map.GetProperty(\"state\").GetString()));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.ListTools_ThroughTheServer_ReturnStructuredMapsAndScans" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_list_paths", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.CreateMapThenFindPaths_Jobs_StoreRankedModuleRootedPaths", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerJobToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"[[\\\"game.exe\\\"+100]+10]+20\", best.Expression);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.CreateMapThenFindPaths_Jobs_StoreRankedModuleRootedPaths" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_list_scans", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.RescanPaths_UnreadableHop_KeepsThePathUnresolvedUnlessDropped", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerJobToolTests.cs", + "scope": "adapter", + "oracle": "Assert.True(fixture.Scans.ListScans().Scans.Single().Incomplete);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.RescanPaths_UnreadableHop_KeepsThePathUnresolvedUnlessDropped" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_load_map", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerFileToolTests.SaveAndLoadMap_UsesAllowedRootAndReportsUnknownImportCompleteness", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerFileToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new PointerEntry(0x1010, 0x2000), Assert.Single(map.Entries));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerFileToolTests.SaveAndLoadMap_UsesAllowedRootAndReportsUnknownImportCompleteness" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_load_scan", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerFileToolTests.SaveAndLoadScan_PreservesContextButInvalidatesOldMatches", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerFileToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([-0x20L, 0x40], restored.Offsets); Assert.Equal(PointerVerification.Unresolved, restored.Verification);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerFileToolTests.SaveAndLoadScan_PreservesContextButInvalidatesOldMatches" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_read_chain", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerChainToolTests.ReadChain_NegativeOffset_ReportsEveryHopAndTheFinalValueInOneDispatch", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerChainToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"21020\", result.Address); Assert.Equal(\"1234\", result.Value);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerChainToolTests.ReadChain_NegativeOffset_ReportsEveryHopAndTheFinalValueInOneDispatch" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_read_chains", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerBatchToolTests.ReadChains_X64SignedOffsetsAndComparison_ReportsStableCandidateOutcomes", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerBatchToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(2, result.Summary.Processed); Assert.Equal(1, result.Summary.Matches);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerBatchToolTests.ReadChains_X64SignedOffsetsAndComparison_ReportsStableCandidateOutcomes" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_rescan_paths", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.RescanPaths_NewMapAndLiveMemory_KeepOnlyPathsThatStillReachTheTarget", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerJobToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new PointerRescanResult(\"hp\", 1, 1, 0, 0, false), live);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerJobToolTests.RescanPaths_NewMapAndLiveMemory_KeepOnlyPathsThatStillReachTheTarget" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_save_map", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerFileToolTests.SaveAndLoadMap_UsesAllowedRootAndReportsUnknownImportCompleteness", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerFileToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(path, saved.FilePath); Assert.True(File.Exists(path));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerFileToolTests.SaveAndLoadMap_UsesAllowedRootAndReportsUnknownImportCompleteness" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "pointer_save_scan", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Pointer.PointerFileToolTests.SaveAndLoadScan_PreservesContextButInvalidatesOldMatches", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerFileToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"MCP.pointer-scan.v2\", saved.Format);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Pointer.PointerFileToolTests.SaveAndLoadScan_PreservesContextButInvalidatesOldMatches" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "process_attach", + "cases": [ + { + "suite": "portable", + "scope": "adapter", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.ProcessListCurrentAndAttach_ForwardTypedRequestsAndMapResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "oracle": "Known typed or fixed-Lua snapshots are asserted against the returned tool result; exact forwarded requests and operation order are also checked.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.ProcessListCurrentAndAttach_ForwardTypedRequestsAndMapResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "process_create", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_FixedEffects_ReturnObservedTypedHostState", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(88, created.ProcessId); Assert.Equal(1L, ReadGlobal(\"created\"));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_FixedEffects_ReturnObservedTypedHostState" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "process_get_current", + "cases": [ + { + "suite": "portable", + "scope": "adapter", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.ProcessListCurrentAndAttach_ForwardTypedRequestsAndMapResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "oracle": "Known typed or fixed-Lua snapshots are asserted against the returned tool result; exact forwarded requests and operation order are also checked.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.ProcessListCurrentAndAttach_ForwardTypedRequestsAndMapResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "process_list", + "cases": [ + { + "suite": "portable", + "scope": "adapter", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.ProcessListCurrentAndAttach_ForwardTypedRequestsAndMapResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs", + "oracle": "Known typed or fixed-Lua snapshots are asserted against the returned tool result; exact forwarded requests and operation order are also checked.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CoreAdapterQualificationTests.ProcessListCurrentAndAttach_ForwardTypedRequestsAndMapResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "process_list_threads", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_FixedEffects_ReturnObservedTypedHostState", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new long[] { 100, 200, 300 }, threads.Threads);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_FixedEffects_ReturnObservedTypedHostState" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "process_open_file", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_FixedEffects_ReturnObservedTypedHostState", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((99, 4096L, fileName, false), opened tuple);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_FixedEffects_ReturnObservedTypedHostState" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "process_save_file", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_FixedEffects_ReturnObservedTypedHostState", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((true, fileName), saved tuple);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_FixedEffects_ReturnObservedTypedHostState" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "process_set_paused", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_Pause_IsRecordedBlocksATargetChangeAndIsResumedByTheRelease", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaProcessPauseTests.cs", + "scope": "adapter", + "oracle": "Assert.NotNull(paused.ResourceId); Assert.True(tools.Resources...);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_Pause_IsRecordedBlocksATargetChangeAndIsResumedByTheRelease" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "process_set_pointer_size", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_FixedEffects_ReturnObservedTypedHostState", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(4, pointerSize.PointerSize);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ProcessV2_FixedEffects_ReturnObservedTypedHostState" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_clear", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordClearToolsTests.Clear_NestedRecords_UsesTheBoundedWalkAndReturnsTheCompleteCount", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordClearToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new RecordClearResult(5), result);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordClearToolsTests.Clear_NestedRecords_UsesTheBoundedWalkAndReturnsTheCompleteCount" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_create", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Create_ValueRecord_CreatesThroughFixedLuaAndWritesTheValueLast", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((1, \"Health\", \"game.exe+20\", \"100\"), (created.Id, created.Description, created.Address, created.Value));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Create_ValueRecord_CreatesThroughFixedLuaAndWritesTheValueLast" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_delete", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Delete_UsesTypedDeleteAfterItCopiesEveryRequestedRecord", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(2, result.Deleted);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Delete_UsesTypedDeleteAfterItCopiesEveryRequestedRecord" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_find", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.ListAndFind_DescribeTheWholeListWithNestedRecords", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([6, 7], page.Records.Select(static record => record.Id).ToArray());", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.ListAndFind_DescribeTheWholeListWithNestedRecords" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_get", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Get_IncludeDropdown_CopiesEveryDropdownInOneBoundedLuaPass", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([mode, linked], result.Records.Select(static record => record.Dropdown).ToArray());", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Get_IncludeDropdown_CopiesEveryDropdownInOneBoundedLuaPass" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_get_selected", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.GetSelected_KnownRecord_MapsTheCurrentClientSnapshot", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Known selected snapshot and exact typed Client call", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.GetSelected_KnownRecord_MapsTheCurrentClientSnapshot" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_group", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Group_ParentedMembers_PreservesRequestedOrderAndRecordsTheClientEffects", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Ordered members, exact group definition and parent requests", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Group_ParentedMembers_PreservesRequestedOrderAndRecordsTheClientEffects" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_list", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.List_PagesTheWholeListThroughTheTypedTableClient", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(3, page.Total);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.List_PagesTheWholeListThroughTheTypedTableClient" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_move", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Move_ParentAndRoot_ForwardTheExactParentRequestsAndReturnMappedRecords", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Parent/root requests and returned records", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Move_ParentAndRoot_ForwardTheExactParentRequestsAndReturnMappedRecords" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_select", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Select_KnownRecord_ForwardsItsIdAndReturnsTheMappedSnapshot", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Selected ID, mapped snapshot and exact Client request", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Select_KnownRecord_ForwardsItsIdAndReturnsTheMappedSnapshot" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_set_active", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.SetActive_ParentThatReachesNoAutoAssemblerRecord_ChangesWithoutTheFeature", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new RecordActivation(5, true, true, false), Assert.Single(result.Records));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.SetActive_ParentThatReachesNoAutoAssemblerRecord_ChangesWithoutTheFeature" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_set_dropdown", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.SetDropdown_ReplacesTheListThroughFixedLuaAndReadsItBack", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((4, stored), (result.Record.Id, result.Record.Dropdown));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.SetDropdown_ReplacesTheListThroughFixedLuaAndReadsItBack" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_set_script", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.SetScript_AutoAssemblerRecord_UsesFixedLuaAndReadsBack", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(12, result.Record.Id);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.SetScript_AutoAssemblerRecord_UsesFixedLuaAndReadsBack" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "record_update", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Update_AddressValueAndOffsets_AppliesAddressThenOffsetsThenValue", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((\"game.exe+100\", \"5\"), (result.Records[0].Address, result.Records[0].Value));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Record.RecordToolsTests.Update_AddressValueAndOffsets_AppliesAddressThenOffsetsThenValue" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "runtime_get_info", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Runtime.RuntimeToolsTests.GetInfo_ReportsEveryGateOfTheActivation", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Runtime/RuntimeToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(expected, info.Gates);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Runtime.RuntimeToolsTests.GetInfo_ReportsEveryGateOfTheActivation(autoAssembler: False, unsafeLua: False, targetCodeExecution: False, kernelAccess: False)", + "CheatEngine.Mcp.Tests.Tools.Runtime.RuntimeToolsTests.GetInfo_ReportsEveryGateOfTheActivation(autoAssembler: False, unsafeLua: True, targetCodeExecution: False, kernelAccess: True)", + "CheatEngine.Mcp.Tests.Tools.Runtime.RuntimeToolsTests.GetInfo_ReportsEveryGateOfTheActivation(autoAssembler: True, unsafeLua: False, targetCodeExecution: True, kernelAccess: False)", + "CheatEngine.Mcp.Tests.Tools.Runtime.RuntimeToolsTests.GetInfo_ReportsEveryGateOfTheActivation(autoAssembler: True, unsafeLua: True, targetCodeExecution: True, kernelAccess: True)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "runtime_get_overview", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Runtime.RuntimeToolsTests.GetOverview_CarriesTheGatesInItsRuntimeSection", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Runtime/RuntimeToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(expected, overview.Runtime.Gates);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Runtime.RuntimeToolsTests.GetOverview_CarriesTheGatesInItsRuntimeSection" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "runtime_list_jobs", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.RuntimeProcessScanV2ContractTests.RuntimeReleaseResources_ReleasesTrackedStateAndRuntimeStopJobRoutesToRegistry", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/RuntimeProcessScanV2ContractTests.cs", + "scope": "adapter", + "oracle": "Assert.Contains(tools.ListJobs(Token).Jobs, status => status.JobId == job.Id);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.RuntimeProcessScanV2ContractTests.RuntimeReleaseResources_ReleasesTrackedStateAndRuntimeStopJobRoutesToRegistry" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "runtime_list_resources", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.RuntimeProcessScanV2ContractTests.RuntimeReleaseResources_ReleasesTrackedStateAndRuntimeStopJobRoutesToRegistry", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/RuntimeProcessScanV2ContractTests.cs", + "scope": "adapter", + "oracle": "Assert.Contains(listed.Resources, item => item.Id == resource.Descriptor.Id);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.RuntimeProcessScanV2ContractTests.RuntimeReleaseResources_ReleasesTrackedStateAndRuntimeStopJobRoutesToRegistry" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "runtime_release_resources", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.RuntimeProcessScanV2ContractTests.RuntimeReleaseResources_ReleasesTrackedStateAndRuntimeStopJobRoutesToRegistry", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/RuntimeProcessScanV2ContractTests.cs", + "scope": "adapter", + "oracle": "Assert.True(released.Release.IsComplete); Assert.Equal(1, resource.ReleaseCalls);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.RuntimeProcessScanV2ContractTests.RuntimeReleaseResources_ReleasesTrackedStateAndRuntimeStopJobRoutesToRegistry" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "runtime_stop_job", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.RuntimeProcessScanV2ContractTests.RuntimeReleaseResources_ReleasesTrackedStateAndRuntimeStopJobRoutesToRegistry", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/RuntimeProcessScanV2ContractTests.cs", + "scope": "adapter", + "oracle": "Assert.True(stopped.Released); Assert.DoesNotContain(tools.ListJobs(Token).Jobs, status => status.JobId == job.Id);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.RuntimeProcessScanV2ContractTests.RuntimeReleaseResources_ReleasesTrackedStateAndRuntimeStopJobRoutesToRegistry" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "scan_delete", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Scan.ScanToolsV2Tests.IndependentSessions_NarrowReadAndDelete_AffectOnlyTheNamedSession", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Scan/ScanToolsV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.True(deleted.Removed); Assert.Null(deleted.Status); Assert.Equal((1, 0, 1, 0), (alpha.NextCalls, beta.NextCalls, alpha.ReleaseCalls, beta.ReleaseCalls));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Scan.ScanToolsV2Tests.IndependentSessions_NarrowReadAndDelete_AffectOnlyTheNamedSession" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "scan_first", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerFirstReadResetAndStop_UseTypedBoundedResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((\"main\", \"ui\", \"Scanning\"), (started.ScannerName, started.Mode, started.State));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerFirstReadResetAndStop_UseTypedBoundedResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "scan_get_status", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerFirstReadResetAndStop_UseTypedBoundedResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"ResultsReady\", tools.GetStatus(...).State);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerFirstReadResetAndStop_UseTypedBoundedResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "scan_list_results", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerFirstReadResetAndStop_UseTypedBoundedResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(3UL, page.Count); Assert.Equal(\"ABCD\", page.Results[0].Address);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerFirstReadResetAndStop_UseTypedBoundedResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "scan_list_scanners", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerStatus_ReportsTheScanSettingsOfTheUi", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaScanOptionsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(expected, tools.ListScanners(Token).Scanners);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerStatus_ReportsTheScanSettingsOfTheUi" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "scan_next", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"Scanning\", status.State); Assert.Equal(expectedIndex, ReadGlobal(\"requestedComparison\"));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex(first: False, comparison: \"between\", value: \"10\", upperValue: \"20\", expectedIndex: 3)", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex(first: False, comparison: \"changed\", value: null, upperValue: null, expectedIndex: 8)", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex(first: False, comparison: \"decreased\", value: null, upperValue: null, expectedIndex: 6)", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex(first: False, comparison: \"decreasedBy\", value: \"10\", upperValue: null, expectedIndex: 7)", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex(first: False, comparison: \"exact\", value: \"10\", upperValue: null, expectedIndex: 0)", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex(first: False, comparison: \"greater\", value: \"10\", upperValue: null, expectedIndex: 1)", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex(first: False, comparison: \"increased\", value: null, upperValue: null, expectedIndex: 4)", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex(first: False, comparison: \"increasedBy\", value: \"10\", upperValue: null, expectedIndex: 5)", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex(first: False, comparison: \"less\", value: \"10\", upperValue: null, expectedIndex: 2)", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerComparison_UsesTheExpectedNativeControlIndex(first: False, comparison: \"unchanged\", value: null, upperValue: null, expectedIndex: 9)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "scan_reset", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerFirstReadResetAndStop_UseTypedBoundedResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"Created\", tools.Reset(...).Status!.State);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerFirstReadResetAndStop_UseTypedBoundedResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "scan_stop", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerFirstReadResetAndStop_UseTypedBoundedResults", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.True(stopped.StopRequested);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ScanV2_MainScannerFirstReadResetAndStop_UseTypedBoundedResults" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "speedhack_get_state", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.SpeedhackV2_SetSpeedOne_AtNormalSpeed_ChangesNothingAndNeverHooks", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaSpeedhackToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(expectedState, tools.GetState(Token));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.SpeedhackV2_SetSpeedOne_AtNormalSpeed_ChangesNothingAndNeverHooks(symbolBefore: False)", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.SpeedhackV2_SetSpeedOne_AtNormalSpeed_ChangesNothingAndNeverHooks(symbolBefore: True)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "speedhack_set_speed", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.SpeedhackV2_SetSpeedOne_WhileTheTargetIsSpedUp_RestoresWithoutActivating", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaSpeedhackToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(expected, result);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.SpeedhackV2_SetSpeedOne_WhileTheTargetIsSpedUp_RestoresWithoutActivating(stubs: \"configuredSpeed = 1; wantedSpeed = 2.5\")", + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.SpeedhackV2_SetSpeedOne_WhileTheTargetIsSpedUp_RestoresWithoutActivating(stubs: \"configuredSpeed = 2.5; wantedSpeed = 2.5\")" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_add_elements", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.StructureAddElements_NewOffsets_ReportTheSortedIndicesAndRollBackARefusal", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaStructureTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([4, 1], change.Indices);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.StructureAddElements_NewOffsets_ReportTheSortedIndicesAndRollBackARefusal" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_autoguess", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.Autoguess_Address_IsResolvedByTheClientAndReadFromTheOffset", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Structures/StructureToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(40, summary.ElementCount);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.Autoguess_Address_IsResolvedByTheClientAndReadFromTheOffset" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_compare", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.Compare_AlongAStructure_UsesItsElementsNamesAndTypes", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Structures/StructureToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"flags\", Assert.Single(result.Rows).Name);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.Compare_AlongAStructure_UsesItsElementsNamesAndTypes" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_create", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.Create_Elements_AreEncodedAsVartypeDisplayAndByteSize", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Structures/StructureToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(2, result.ElementCount);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.Create_Elements_AreEncodedAsVartypeDisplayAndByteSize" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_delete", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.StructureDelete_KnownThenAgain_DestroysOnceThenIsNotFound", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaStructureTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new StructureDeleted(\"Enemy\"), tools.Delete(\"Enemy\", StructureToken));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.StructureDelete_KnownThenAgain_DestroysOnceThenIsNotFound" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_fill_from_dotnet", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.StructureFillFromDotNet_HaltedMissingOrFound_FollowsTheCollector", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaStructureTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new StructureSummary(\"Game.Player\", 8, 1), filled);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.StructureFillFromDotNet_HaltedMissingOrFound_FollowsTheCollector" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_generate_c_header", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Structures.StructureHeaderToolsTests.GenerateCHeader_CheatEngineText_IsReturnedWithEveryDeclaredStructure", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Structures/StructureHeaderToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Contains(\"struct Player\", result.Text, StringComparison.Ordinal);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Structures.StructureHeaderToolsTests.GenerateCHeader_CheatEngineText_IsReturnedWithEveryDeclaredStructure" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_get", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.Get_CheatEngineElementFields_MapToContractTypesAndOffsets", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Structures/StructureToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"health\", result.Elements[0].Name);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.Get_CheatEngineElementFields_MapToContractTypesAndOffsets" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_get_pdb_layout", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.GetPdbLayout_Fields_MapOffsetsAndReportedTypes", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Structures/StructureToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((\"4\", \"int32\"), (result.Fields[0].Offset, result.Fields[0].ValueType));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.GetPdbLayout_Fields_MapOffsetsAndReportedTypes" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_list", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.List_Filter_ReachesTheScriptAsArgumentsAndItsPageIsReturned", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Structures/StructureToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"Player\", Assert.Single(result.Structures).Name);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.List_Filter_ReachesTheScriptAsArgumentsAndItsPageIsReturned" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_read", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.Read_TwoAddresses_DecodesEveryElementAndNullsWhatIsUnreadable", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Structures/StructureToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([\"7\", \"8\"], result.Elements[0].Values);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.Read_TwoAddresses_DecodesEveryElementAndNullsWhatIsUnreadable" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_remove_elements", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.StructureRemoveElements_Indices_RemoveHighestFirstAndRefuseOutOfRange", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaStructureTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([2, 0], change.Indices);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.StructureRemoveElements_Indices_RemoveHighestFirstAndRefuseOutOfRange" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_set_name", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.StructureSetName_StubbedStructures_RenamesInPlaceAndRefusesTakenOrMissingNames", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaStructureTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new StructureSummary(\"Hero\", 32, 4), renamed);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.StructureSetName_StubbedStructures_RenamesInPlaceAndRefusesTakenOrMissingNames" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_update_elements", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.UpdateElements_DisplayWithoutType_IsCheckedByTheScriptAgainstTheCurrentType", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Structures/StructureToolsTests.cs", + "scope": "adapter", + "oracle": "The fixed-Lua request encodes hexadecimal flags and the dtHexadecimal type; returned change indices equal [1].", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.UpdateElements_DisplayWithoutType_IsCheckedByTheScriptAgainstTheCurrentType" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "structure_write_element", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.WriteElement_Int32_WritesTheEncodedBytesAtTheElementAndReadsThemBack", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Structures/StructureToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"9\", result.Value);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Structures.StructureToolsTests.WriteElement_Int32_WritesTheEncodedBytesAtTheElementAndReadsThemBack" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "symbol_add_module", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolToolTests.AddModule_LocalFile_LoadsTheNormalizedPathAtTheResolvedBase", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Symbol/SymbolToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new SymbolModuleLoad(pdb, \"140000000\", true), load); Assert.Equal(1, target.Dispatcher.Calls);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolToolTests.AddModule_LocalFile_LoadsTheNormalizedPathAtTheResolvedBase" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "symbol_enable_sources", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolToolTests.EnableSources_Windows_ReportsExternalAccessAndUsesOnlyFixedLuaArguments", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Symbol/SymbolToolTests.cs", + "scope": "adapter", + "oracle": "Known Windows enabled/kernel disabled/external access result and exact boolean fixed-Lua arguments.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolToolTests.EnableSources_Windows_ReportsExternalAccessAndUsesOnlyFixedLuaArguments" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "symbol_find", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolFindToolTests.Find_WithoutModule_ChecksTheProcessAndPassesTheTextOnlyAsAnArgument", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Symbol/SymbolFindToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"playerHealth\", Assert.Single(result.Symbols).Name);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolFindToolTests.Find_WithoutModule_ChecksTheProcessAndPassesTheTextOnlyAsAnArgument" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "symbol_get_module_preference", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ModuleSymbolGetModulePreference_Stubbed_CopiesAtMost1024NamesAndRefusesAMissingHostList", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaModuleSymbolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([\"game\", \"engine\", \"kernel32\"], current.Modules);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ModuleSymbolGetModulePreference_Stubbed_CopiesAtMost1024NamesAndRefusesAMissingHostList" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "symbol_list_registered", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ModuleSymbolListRegistered_Stubbed_CopiesEveryFieldIntoTheRecord", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaModuleSymbolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new RegisteredSymbolEntry(\"playerBase\", false, \"140000010\"), list.Symbols[0]);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ModuleSymbolListRegistered_Stubbed_CopiesEveryFieldIntoTheRecord" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "symbol_register", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolRegistrationToolTests.Register_Symbol_IsAnOwnedLeaseTrackedAsASymbolResource", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Symbol/SymbolRegistrationToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((\"playerBase\", \"400010\", false), (symbol.Name, symbol.Address, symbol.DoNotSave));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolRegistrationToolTests.Register_Symbol_IsAnOwnedLeaseTrackedAsASymbolResource" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "symbol_reload", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolToolTests.Reload_Scope_StartsTheReloadAndReportsDone", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Symbol/SymbolToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal((scope, true), (result.Scope, result.Done));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolToolTests.Reload_Scope_StartsTheReloadAndReportsDone(scope: All, module: null, arguments: \"[1] = \\\"all\\\", [2] = nil\")", + "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolToolTests.Reload_Scope_StartsTheReloadAndReportsDone(scope: Dotnet, module: \"Assembly-CSharp.dll\", arguments: \"[1] = \\\"dotnet\\\", [2] = \\\"Assembly-CSharp.dll\\\"\")", + "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolToolTests.Reload_Scope_StartsTheReloadAndReportsDone(scope: NewModules, module: null, arguments: \"[1] = \\\"new_modules\\\", [2] = nil\")" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "symbol_resolve", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolToolTests.Resolve_Expressions_ResolveInBandWithNamesSymbolsAndPerItemErrors", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Symbol/SymbolToolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(\"401010\", result.Items[0].Address);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolToolTests.Resolve_Expressions_ResolveInBandWithNamesSymbolsAndPerItemErrors" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "symbol_set_module_preference", + "cases": [ + { + "suite": "nativeLua", + "testMethod": "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ModuleSymbolSetModulePreference_Stubbed_MovesModulesFirstOrReplacesAndReadsBack", + "sourceFile": "tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaModuleSymbolTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal([\"solo\"], replaced.Modules);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.NativeLua.NativeLuaToolRuntimeTests.ModuleSymbolSetModulePreference_Stubbed_MovesModulesFirstOrReplacesAndReadsBack" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "symbol_unregister", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolRegistrationToolTests.Unregister_OwnedSymbol_ReleasesItsLeaseAndForgetsIt", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Symbol/SymbolRegistrationToolTests.cs", + "scope": "adapter", + "oracle": "Assert.True(result.Release.IsComplete);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Symbol.SymbolRegistrationToolTests.Unregister_OwnedSymbol_ReleasesItsLeaseAndForgetsIt" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "table_list_files", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ListFiles_AllowedRoots_ReturnsOnlyTablesAndSkipsNestedJunctions", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(2, result.Total); Assert.Equal([\"first.ct\", \"second.XML\"], names);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeAsmTableV2Tests.ListFiles_AllowedRoots_ReturnsOnlyTablesAndSkipsNestedJunctions" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "table_load", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.TableLoad_InspectedDataTable_ForwardsPinnedTrustedFileAndMerge", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeTableQualificationTests.cs", + "scope": "adapter", + "oracle": "One inspected XML record without executable content, trusted file/merge forwarding, one Client load, unchanged source.", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.TableLoad_InspectedDataTable_ForwardsPinnedTrustedFileAndMerge(merge: False)", + "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.TableLoad_InspectedDataTable_ForwardsPinnedTrustedFileAndMerge(merge: True)" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "table_save", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.TableSave_AllowedDestination_ForwardsTheTrustedPathAndReportsTheCreatedFile", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeTableQualificationTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(new TableSaveResult(destination, false), result); Assert.Equal(\"\", File.ReadAllText(destination));", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.TableSave_AllowedDestination_ForwardsTheTrustedPathAndReportsTheCreatedFile" + ] + }, + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.TableSave_AllowedDestination_ForwardsTheTrustedPathAndReportsTheCreatedFile", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/CodeTableQualificationTests.cs", + "scope": "adapter", + "oracle": "Trusted destination and independently created file", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.CodeTableQualificationTests.TableSave_AllowedDestination_ForwardsTheTrustedPathAndReportsTheCreatedFile" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "util_calculate", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Util.UtilToolsTests.Calculate_UsesUnsigned64BitWraparound", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Util/UtilToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(unsignedValue, result.UnsignedValue); Assert.Equal(signedValue, result.SignedValue);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Util.UtilToolsTests.Calculate_UsesUnsigned64BitWraparound(expression: \"(1 << 63) + (1 << 63)\", unsignedValue: \"0\", signedValue: \"0\", hex: \"0\")", + "CheatEngine.Mcp.Tests.Tools.Util.UtilToolsTests.Calculate_UsesUnsigned64BitWraparound(expression: \"~0\", unsignedValue: \"18446744073709551615\", signedValue: \"-1\", hex: \"FFFFFFFFFFFFFFFF\")", + "CheatEngine.Mcp.Tests.Tools.Util.UtilToolsTests.Calculate_UsesUnsigned64BitWraparound(expression: \"0x8000000000000000 >> 1\", unsignedValue: \"4611686018427387904\", signedValue: \"4611686018427387904\", hex: \"4000000000000000\")", + "CheatEngine.Mcp.Tests.Tools.Util.UtilToolsTests.Calculate_UsesUnsigned64BitWraparound(expression: \"0xFFFFFFFFFFFFFFFF + 2\", unsignedValue: \"1\", signedValue: \"1\", hex: \"1\")" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + }, + { + "name": "util_convert_value", + "cases": [ + { + "suite": "portable", + "testMethod": "CheatEngine.Mcp.Tests.Tools.Util.UtilToolsTests.ConvertValue_Scalar_UsesTheRequestedByteOrderAndTargetInterpretation", + "sourceFile": "tests/CheatEngine.Mcp.Tests/Tools/Util/UtilToolsTests.cs", + "scope": "adapter", + "oracle": "Assert.Equal(expected, result.Value);", + "requiredCases": [ + "CheatEngine.Mcp.Tests.Tools.Util.UtilToolsTests.ConvertValue_Scalar_UsesTheRequestedByteOrderAndTargetInterpretation" + ] + } + ], + "remaining": "Native success, side effects and cleanup on the exact product bundle remain unverified." + } + ] +} diff --git a/docs/qualification/all-tools/live-evidence.json b/docs/qualification/all-tools/live-evidence.json new file mode 100644 index 0000000..ef6b932 --- /dev/null +++ b/docs/qualification/all-tools/live-evidence.json @@ -0,0 +1,339 @@ +{ + "schema": "cheatengine-mcp-live-evidence/v1", + "boundary": "Reviewed live positive evidence only. A row is verified only when its receipt, any required cleanup receipt, and a fully passed/restored live summary are present.", + "tools": [ + { + "name": "runtime_get_info", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyGatesAsync", + "oracle": "gates unsafeLua, autoAssembler, targetCodeExecution, kernelAccess are all false", + "receipt": "live_workflow_gates", + "cleanupReceipt": "none", + "scope": "safe_gate" + }, + { + "name": "memory_allocate", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyMemoryAsync", + "oracle": "source/destination size == 128 and source executable == false", + "receipt": "live_workflow_memory", + "cleanupReceipt": "live_workflow_memory_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "memory_write_batch", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyMemoryAsync", + "oracle": "written == 3; verified == true; mismatched == null", + "receipt": "live_workflow_memory", + "cleanupReceipt": "live_workflow_memory_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "memory_read", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyMemoryAsync", + "oracle": "InitialBatchValues and scalar round-trips assert exact values including endian/string/wide/64-bit", + "receipt": "live_workflow_memory", + "cleanupReceipt": "live_workflow_memory_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "memory_read_batch", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyMemoryAsync", + "oracle": "failed == 1; readable items equal 1 and MCP; unreadable item error == memory_read_failed", + "receipt": "live_workflow_memory", + "cleanupReceipt": "live_workflow_memory_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "memory_hash", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyMemoryAsync", + "oracle": "hash equals SHA256 of known 16-byte buffer", + "receipt": "live_workflow_memory", + "cleanupReceipt": "live_workflow_memory_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "memory_copy", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyMemoryAsync", + "oracle": "returned source and destination equal requested addresses; subsequent MemoryCompare reports equal with no differences for the known 16-byte buffer", + "receipt": "live_workflow_memory", + "cleanupReceipt": "live_workflow_memory_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "memory_compare", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyMemoryAsync", + "oracle": "equal == true; truncated == false; differences empty", + "receipt": "live_workflow_memory", + "cleanupReceipt": "live_workflow_memory_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "memory_create_snapshot", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyMemoryAsync", + "oracle": "unreadableBytes == 0 and unreadable empty", + "receipt": "live_workflow_memory", + "cleanupReceipt": "live_workflow_memory_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "memory_compare_snapshot", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyMemoryAsync", + "oracle": "compared/skipped/total == (4,0,1) and changed address equals destination", + "receipt": "live_workflow_memory", + "cleanupReceipt": "live_workflow_memory_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "memory_set_protection", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyMemoryAsync", + "oracle": "current read true, write false, execute false", + "receipt": "live_workflow_memory", + "cleanupReceipt": "live_workflow_memory_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "scan_get_status", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyScannerAsync", + "oracle": "main settings memPrivate is true", + "receipt": "live_workflow_scanner", + "cleanupReceipt": "live_workflow_scanner_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "scan_first", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyScannerAsync", + "oracle": "successful first scan is followed by AssertScan exact result", + "receipt": "live_workflow_scanner", + "cleanupReceipt": "live_workflow_scanner_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "scan_next", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyScannerAsync", + "oracle": "changed/increased/decreased transitions each followed by AssertScan exact address/value", + "receipt": "live_workflow_scanner", + "cleanupReceipt": "live_workflow_scanner_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "scan_list_results", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "AssertScanAsync", + "oracle": "single result address/value; hasMore false", + "receipt": "live_workflow_scanner", + "cleanupReceipt": "live_workflow_scanner_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "scan_reset", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyScannerAsync", + "oracle": "removed == false; subsequent ScanFirst succeeds on the reset scanner and returns exactly the expected address/value 25 for the range 20..30", + "receipt": "live_workflow_scanner", + "cleanupReceipt": "live_workflow_scanner_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "aob_find", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyAobAsync", + "oracle": "two exact patterns and AssertAob each match at address+16", + "receipt": "live_workflow_aob", + "cleanupReceipt": "live_workflow_aob_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "aob_find_value", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyAobAsync", + "oracle": "result pattern == 04 03 02 01 and AssertAob at address+32", + "receipt": "live_workflow_aob", + "cleanupReceipt": "live_workflow_aob_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "code_get_function_graph", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyControlFlowAsync", + "oracle": "entry/start/end, instructionCount 7, 3 blocks and terminators/successors asserted; truncation case asserted", + "receipt": "live_workflow_cfg", + "cleanupReceipt": "live_workflow_cfg_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "module_list", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyModulesAsync", + "oracle": "expected module found; base and size > 0; is64Bit matches sandbox", + "receipt": "live_workflow_modules", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "module_get", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyModulesAsync", + "oracle": "base matches and sections include executable; PE machine matches architecture", + "receipt": "live_workflow_modules", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "module_list_imports", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyModulesAsync", + "oracle": "total > 0 and each import dll nonblank", + "receipt": "live_workflow_modules", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "symbol_resolve", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyModulesAsync", + "oracle": "two items; no errors; both address equal module base", + "receipt": "live_workflow_modules", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "module_find_patches", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyModulesAsync", + "oracle": "module matches; comparedBytes > 0; unreadable == 0; truncated false", + "receipt": "live_workflow_modules", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "structure_create", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyStructuresAsync", + "oracle": "elementCount == 2 and size >= 6", + "receipt": "live_workflow_structures", + "cleanupReceipt": "live_workflow_structures_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "structure_read", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyStructuresAsync", + "oracle": "health and flags rows equal HealthValues/FlagValues", + "receipt": "live_workflow_structures", + "cleanupReceipt": "live_workflow_structures_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "structure_compare", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyStructuresAsync", + "oracle": "row offset/name/classification == (4,flags,discriminator)", + "receipt": "live_workflow_structures", + "cleanupReceipt": "live_workflow_structures_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "structure_write_element", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyStructuresAsync", + "oracle": "returned value == 9 and subsequent memory_read == 9", + "receipt": "live_workflow_structures", + "cleanupReceipt": "live_workflow_structures_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "structure_generate_c_header", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveWorkflowQualification.cs", + "testMethod": "VerifyStructuresAsync", + "oracle": "generator managed; text contains structure name, int32_t health, uint16_t flags", + "receipt": "live_workflow_structures", + "cleanupReceipt": "live_workflow_structures_cleanup", + "scope": "reversible_workflow" + }, + { + "name": "process_get_current", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs", + "testMethod": "VerifyAsync", + "oracle": "isOpen true; PID equals owned HostA target; pointer size equals target architecture; selectionEpoch >= 0", + "receipt": "all_tools_process_get_current", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "process_list", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs", + "testMethod": "VerifyAsync", + "oracle": "target PID present; truncated false", + "receipt": "all_tools_process_list", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "process_list_threads", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs", + "testMethod": "VerifyAsync", + "oracle": "threads nonempty and every thread ID > 0; truncated false", + "receipt": "all_tools_process_list_threads", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "process_set_pointer_size", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs", + "testMethod": "VerifyAsync", + "oracle": "returned and subsequent current pointerSize equal captured width; PID and selection epoch are unchanged", + "receipt": "all_tools_process_set_pointer_size", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "memory_get_address_info", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs", + "testMethod": "VerifyAsync", + "oracle": "single item no error; committed/private region readable+writable; pointerValue present", + "receipt": "all_tools_memory_get_address_info", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "memory_read_samples", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs", + "testMethod": "VerifyAsync", + "oracle": "ticks > 0; one series; exact address, sample count, value 20260926, no errors/truncation", + "receipt": "all_tools_memory_read_samples", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "lua_find_api", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs", + "testMethod": "VerifyAsync", + "oracle": "total > 0; lines nonempty; every line contains getAddressSafe and positive line number", + "receipt": "all_tools_lua_find_api", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + }, + { + "name": "code_disassemble_bytes", + "sourceFile": "tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs", + "testMethod": "VerifyAsync", + "oracle": "90 returns text containing nop; C3 returns text containing ret; origin 0", + "receipt": "all_tools_code_disassemble_bytes", + "cleanupReceipt": "none", + "scope": "read_only_workflow" + } + ] +} diff --git a/eng/Write-AllToolQualificationReport.ps1 b/eng/Write-AllToolQualificationReport.ps1 new file mode 100644 index 0000000..915f884 --- /dev/null +++ b/eng/Write-AllToolQualificationReport.ps1 @@ -0,0 +1,269 @@ +#requires -Version 7.0 +<# +.SYNOPSIS +Writes one evidence row per advertised tool without treating routing or refusals as native qualification. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)][string] $GatewayTrx, + [string] $PortableTrx, + [string] $NativeLuaTrx, + [string] $LiveSummary, + [string] $LiveEvidencePath = (Join-Path $PSScriptRoot '../docs/qualification/all-tools/live-evidence.json'), + [string] $OutputDirectory = (Join-Path $PSScriptRoot '../artifacts/all-tools/report') +) +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +function Read-TestReport([string] $Path) { + $settings = [Xml.XmlReaderSettings]::new() + $settings.DtdProcessing = [Xml.DtdProcessing]::Prohibit + $settings.XmlResolver = $null + $reader = [Xml.XmlReader]::Create([IO.Path]::GetFullPath($Path), $settings) + try { + $document = [Xml.XmlDocument]::new() + $document.Load($reader) + $namespace = [Xml.XmlNamespaceManager]::new($document.NameTable) + $namespace.AddNamespace('t', 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010') + $results = @($document.SelectNodes('/t:TestRun/t:Results/t:UnitTestResult', $namespace)) + if ($results.Count -eq 0) { throw "No test results in '$Path'." } + $cases = @{} + $occurrences = @{} + foreach ($result in $results) { + $name = $result.GetAttribute('testName') + $prior = if ($occurrences.ContainsKey($name)) { [int] $occurrences[$name] } else { 0 } + $occurrences[$name] = 1 + $prior + $outcome = $result.GetAttribute('outcome') + if (-not $cases.ContainsKey($name) -or (Outcome-Rank $outcome) -gt (Outcome-Rank $cases[$name])) { + $cases[$name] = $outcome + } + } + return @{ + Cases = $cases + Occurrences = $occurrences + Summary = [ordered]@{ + path = [IO.Path]::GetFullPath($Path) + sha256 = (Get-FileHash -LiteralPath $Path).Hash + total = $results.Count + passed = @($results | Where-Object { $_.GetAttribute('outcome') -eq 'Passed' }).Count + allPassed = @($results | Where-Object { $_.GetAttribute('outcome') -ne 'Passed' }).Count -eq 0 + } + } + } + finally { $reader.Dispose() } +} + +$repository = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +$catalogPath = Join-Path $repository 'tests/CheatEngine.Mcp.Tests/Contract/Golden/backend-tools.json' +$evidencePath = Join-Path $repository 'docs/qualification/all-tools/behavior-evidence.json' +$liveEvidencePath = [IO.Path]::GetFullPath($LiveEvidencePath) +$catalog = @(Get-Content -LiteralPath $catalogPath -Raw | ConvertFrom-Json -AsHashtable) +$evidence = Get-Content -LiteralPath $evidencePath -Raw | ConvertFrom-Json -AsHashtable +$liveEvidence = Get-Content -LiteralPath $liveEvidencePath -Raw | ConvertFrom-Json -AsHashtable + +function Outcome-Rank([string] $Outcome) { + switch ($Outcome) { + 'Passed' { return 0 } + 'NotExecuted' { return 1 } + 'Skipped' { return 1 } + 'Inconclusive' { return 2 } + 'Failed' { return 3 } + 'Missing' { return 4 } + 'MissingOrDuplicate' { return 4 } + default { return 3 } + } +} + +function Select-WorstOutcome([string[]] $Outcomes) { + if ($Outcomes.Count -eq 0) { return 'Missing' } + return @($Outcomes | Sort-Object { Outcome-Rank $_ } -Descending | Select-Object -First 1)[0] +} + +function Get-TestOutcome($Report, $Case) { + $outcomes = foreach ($name in @($Case['requiredCases'])) { + if ($null -eq $Report -or -not $Report.Cases.ContainsKey($name) -or $Report.Occurrences[$name] -ne 1) { + [ordered]@{ test = $name; outcome = 'MissingOrDuplicate' } + } + else { + [ordered]@{ test = $name; outcome = $Report.Cases[$name] } + } + } + return [ordered]@{ outcome = Select-WorstOutcome @($outcomes | ForEach-Object { $_['outcome'] }); cases = @($outcomes) } +} + +function Get-ExactTestOutcome($Report, [string] $Method) { + if ($null -eq $Report) { return 'MissingOrDuplicate' } + $matches = @($Report.Cases.Keys | Where-Object { + $_ -eq $Method -or ($_.StartsWith($Method, [StringComparison]::Ordinal) -and + $_.Length -gt $Method.Length -and $_[$Method.Length] -eq '(') + }) + if ($matches.Count -ne 1 -or $Report.Occurrences[$matches[0]] -ne 1) { return 'MissingOrDuplicate' } + return $Report.Cases[$matches[0]] +} + +function Assert-BehaviorEvidence($Evidence, $Catalog, [string] $Repository) { + if ($Evidence['schema'] -ne 'cheatengine-mcp-behavior-evidence/v1') { + throw "Unsupported behavior evidence schema '$($Evidence['schema'])'." + } + if ($null -eq $Evidence['boundary'] -or [string]::IsNullOrWhiteSpace([string] $Evidence['boundary'])) { + throw 'Behavior evidence requires a non-empty boundary.' + } + $expected = @($Catalog | ForEach-Object { [string] $_['name'] }) + $tools = @($Evidence['tools']) + $actual = @($tools | ForEach-Object { [string] $_['name'] }) + if ($actual.Count -ne $expected.Count -or @($actual | Sort-Object -Unique).Count -ne $actual.Count -or + @(Compare-Object ($expected | Sort-Object) ($actual | Sort-Object)).Count -ne 0) { + throw 'Behavior evidence tool names must be the exact unique golden backend catalog names.' + } + $root = [IO.Path]::GetFullPath($Repository).TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar + foreach ($tool in $tools) { + if (-not $tool.ContainsKey('cases') -or -not $tool.ContainsKey('remaining')) { + throw "Behavior evidence '$($tool['name'])' requires cases and remaining." + } + foreach ($case in @($tool['cases'])) { + foreach ($key in @('suite', 'testMethod', 'requiredCases', 'sourceFile', 'scope', 'oracle')) { + if (-not $case.ContainsKey($key) -or [string]::IsNullOrWhiteSpace([string] $case[$key])) { + throw "Behavior evidence '$($tool['name'])' has a case without $key." + } + } + if ($case['suite'] -notin @('portable', 'nativeLua')) { throw "Unknown suite '$($case['suite'])'." } + if ($case['scope'] -notin @('adapter', 'fixed_lua_body')) { throw "Unknown scope '$($case['scope'])'." } + $requiredCases = @($case['requiredCases']) + if ($requiredCases.Count -eq 0 -or @($requiredCases | Sort-Object -Unique).Count -ne $requiredCases.Count) { + throw "Behavior evidence '$($tool['name'])' requires non-empty unique requiredCases." + } + foreach ($required in $requiredCases) { + if ([string]::IsNullOrWhiteSpace([string] $required) -or + -not ($required -eq $case['testMethod'] -or ($required.StartsWith($case['testMethod'], [StringComparison]::Ordinal) -and + $required.Length -gt $case['testMethod'].Length -and $required[$case['testMethod'].Length] -eq '('))) { + throw "Behavior evidence '$($tool['name'])' has a required case outside testMethod '$($case['testMethod'])'." + } + } + if ([IO.Path]::IsPathRooted([string] $case['sourceFile'])) { throw "sourceFile must be repository-relative." } + $source = [IO.Path]::GetFullPath((Join-Path $Repository $case['sourceFile'])) + if (-not $source.StartsWith($root, [StringComparison]::OrdinalIgnoreCase) -or -not (Test-Path -LiteralPath $source -PathType Leaf)) { + throw "sourceFile '$($case['sourceFile'])' is not an existing repository file." + } + } + } +} + +function Assert-LiveEvidence($LiveMap, $Catalog, [string] $Repository) { + if ($LiveMap['schema'] -ne 'cheatengine-mcp-live-evidence/v1') { + throw "Unsupported live evidence schema '$($LiveMap['schema'])'." + } + if ([string]::IsNullOrWhiteSpace([string] $LiveMap['boundary'])) { throw 'Live evidence requires a non-empty boundary.' } + $names = @($LiveMap['tools'] | ForEach-Object { [string] $_['name'] }) + $catalogNames = @($Catalog | ForEach-Object { [string] $_['name'] }) + if ($names.Count -ne 37 -or @($names | Sort-Object -Unique).Count -ne $names.Count -or + @($names | Where-Object { $_ -notin $catalogNames }).Count -ne 0 -or $names -contains 'memory_dump_to_file') { + throw 'Live evidence must contain 37 unique golden backend tool names and exclude memory_dump_to_file.' + } + $root = [IO.Path]::GetFullPath($Repository).TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar + foreach ($entry in @($LiveMap['tools'])) { + foreach ($key in @('name', 'sourceFile', 'testMethod', 'oracle', 'receipt', 'cleanupReceipt', 'scope')) { + if (-not $entry.ContainsKey($key) -or [string]::IsNullOrWhiteSpace([string] $entry[$key])) { + throw "Live evidence '$($entry['name'])' has no $key." + } + } + if ($entry['scope'] -notin @('safe_gate', 'reversible_workflow', 'read_only_workflow')) { + throw "Unknown live evidence scope '$($entry['scope'])'." + } + if ([IO.Path]::IsPathRooted([string] $entry['sourceFile'])) { throw 'Live sourceFile must be repository-relative.' } + $source = [IO.Path]::GetFullPath((Join-Path $Repository $entry['sourceFile'])) + if (-not $source.StartsWith($root, [StringComparison]::OrdinalIgnoreCase) -or -not (Test-Path -LiteralPath $source -PathType Leaf)) { + throw "Live sourceFile '$($entry['sourceFile'])' is not an existing repository file." + } + } +} + +function Read-LiveReceipts([string] $SummaryPath) { + $path = Join-Path (Split-Path -Parent ([IO.Path]::GetFullPath($SummaryPath))) 'receipts.jsonl' + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Live receipt file '$path' is missing." } + $counts = @{} + foreach ($line in Get-Content -LiteralPath $path) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $entry = $line | ConvertFrom-Json -AsHashtable + if (-not $entry.ContainsKey('name') -or [string]::IsNullOrWhiteSpace([string] $entry['name'])) { throw "Live receipt file '$path' has an unnamed row." } + $name = [string] $entry['name'] + $prior = if ($counts.ContainsKey($name)) { [int] $counts[$name] } else { 0 } + $counts[$name] = 1 + $prior + } + return @{ path = $path; sha256 = (Get-FileHash -LiteralPath $path).Hash; counts = $counts } +} + +Assert-BehaviorEvidence $evidence $catalog $repository +Assert-LiveEvidence -LiveMap $liveEvidence -Catalog $catalog -Repository $repository +$gateway = Read-TestReport $GatewayTrx +$suiteReports = [ordered]@{ gateway = $gateway.Summary } +$behaviorReports = @{} +foreach ($entry in @(@{ Name = 'portable'; Path = $PortableTrx }, @{ Name = 'nativeLua'; Path = $NativeLuaTrx })) { + if ($entry.Path) { + $parsed = Read-TestReport $entry.Path + $suiteReports[$entry.Name] = $parsed.Summary + $behaviorReports[$entry.Name] = $parsed + } +} +$live = if ($LiveSummary) { Get-Content -LiteralPath $LiveSummary -Raw | ConvertFrom-Json -AsHashtable } else { @{} } +$liveReceipts = if ($LiveSummary) { Read-LiveReceipts $LiveSummary } else { $null } +$livePassed = $live['passed'] -eq $true -and $live['userStateRestored'] -eq $true -and + $live['sourceInstallationUnchanged'] -eq $true -and $live.ContainsKey('cleanupFailures') -and + @($live['cleanupFailures']).Count -eq 0 -and $live['schema'] -eq 'cheatengine-mcp-live-multi-instance/v1' -and + $live.ContainsKey('selected_distribution') -and $live['selected_distribution'].ContainsKey('productVersion') -and + $live['selected_distribution'].ContainsKey('pluginSha256') -and $live['selected_distribution'].ContainsKey('gatewaySha256') +$methods = @('CallTool_MissingInstanceId_RejectsEveryBackendToolBeforeAnyBackendContact', + 'CallTool_UnknownInstanceId_RejectsEveryBackendToolWithoutFallback', + 'CallTool_StaleIdentity_RejectsEveryBackendToolBeforeMutation', + 'CallTool_TwoInstances_RoutesEveryBackendToolWithOnlyInstanceIdRemoved') +$evidenceByTool = @{} +foreach ($entry in @($evidence['tools'])) { $evidenceByTool[$entry['name']] = $entry } +$liveEvidenceByTool = @{} +foreach ($entry in @($liveEvidence['tools'])) { $liveEvidenceByTool[$entry['name']] = $entry } +$tools = @($catalog) + @(@{ name = 'instance_list'; title = 'List instances'; _meta = @{} }) +$rows = foreach ($tool in $tools | Sort-Object { $_['name'] }) { + $name = $tool['name'] + $routingCases = if ($name -eq 'instance_list') { + @('InstanceList_EmptyRegistry_ReturnsLocalEmptyDiscoveryResult', 'InstanceList_PublishedDoubles_ReturnsBothVerifiedInstancesWithoutTokens') | + ForEach-Object { [ordered]@{ test = "CheatEngine.Mcp.Tests.Hosting.AllToolGatewayQualificationTests.$_"; outcome = Get-ExactTestOutcome $gateway "CheatEngine.Mcp.Tests.Hosting.AllToolGatewayQualificationTests.$_" } } + } else { + $methods | ForEach-Object { + $method = "CheatEngine.Mcp.Tests.Hosting.AllToolGatewayQualificationTests.$_" + $test = "$method(tool: `"$name`")" + [ordered]@{ test = $test; outcome = Get-ExactTestOutcome $gateway $test } + } + } + $sourceCases = if ($name -eq 'instance_list') { @() } else { @($evidenceByTool[$name]['cases']) } + $asEvidence = foreach ($case in $sourceCases) { + $testOutcome = Get-TestOutcome $behaviorReports[$case['suite']] $case + [ordered]@{ suite = $case['suite']; test = $case['testMethod']; selectors = @($case['requiredCases']); sourceFile = $case['sourceFile']; scope = $case['scope']; oracle = $case['oracle']; outcome = $testOutcome['outcome']; caseOutcomes = $testOutcome['cases'] } + } + $adapterEvidence = @($asEvidence | Where-Object { $_['scope'] -eq 'adapter' }) + $fixedEvidence = @($asEvidence | Where-Object { $_['scope'] -eq 'fixed_lua_body' }) + $liveCase = $liveEvidenceByTool[$name] + $liveReceiptPresent = $null -ne $liveCase -and $liveReceipts -and $liveReceipts.counts[$liveCase['receipt']] -eq 1 + $cleanupReceiptPresent = $null -ne $liveCase -and ($liveCase['cleanupReceipt'] -eq 'none' -or + ($liveReceipts -and $liveReceipts.counts[$liveCase['cleanupReceipt']] -eq 1)) + [ordered]@{ + name = $name; title = $tool['title']; requiredFeatures = @($tool['_meta']['cheatengine/requires'] | Where-Object { $_ }) + routing = if (@($routingCases | Where-Object { $_.outcome -ne 'Passed' }).Count -eq 0) { 'passed' } else { 'incomplete' } + routingCases = $routingCases + behavior = if ($adapterEvidence.Count -gt 0 -and @($adapterEvidence | Where-Object { $_.outcome -ne 'Passed' }).Count -eq 0) { 'verified_with_doubles' } else { 'requires_reviewed_per_tool_evidence' } + adapterEvidence = $adapterEvidence; fixedLuaBodyEvidence = $fixedEvidence + remaining = if ($name -eq 'instance_list') { 'Local gateway discovery only; no backend behavior applies.' } else { $evidenceByTool[$name]['remaining'] } + live = if ($null -eq $liveCase) { 'not_verified' } elseif ($livePassed -and $liveReceiptPresent -and $cleanupReceiptPresent) { 'verified' } elseif (-not $livePassed -and $liveReceiptPresent) { 'observed_in_failed_session' } else { 'not_verified' } + liveEvidence = if ($null -eq $liveCase) { $null } else { [ordered]@{ scope = $liveCase['scope']; sourceFile = $liveCase['sourceFile']; testMethod = $liveCase['testMethod']; oracle = $liveCase['oracle']; receipt = $liveCase['receipt']; cleanupReceipt = $liveCase['cleanupReceipt']; receiptPresent = $liveReceiptPresent; cleanupReceiptPresent = $cleanupReceiptPresent } } + } +} +$report = [ordered]@{ + schema = 'cheatengine-mcp-all-tools/v1'; generatedUtc = [DateTimeOffset]::UtcNow.ToString('O') + catalogSha256 = (Get-FileHash -LiteralPath $catalogPath).Hash; evidenceSha256 = (Get-FileHash -LiteralPath $evidencePath).Hash; liveEvidenceSha256 = (Get-FileHash -LiteralPath $liveEvidencePath).Hash + toolCount = $rows.Count; routingComplete = @($rows | Where-Object { $_.routing -ne 'passed' }).Count -eq 0 + nativeQualificationComplete = $false + boundaries = @('Routing uses loopback doubles; it does not execute backend tools.', 'Fixed Lua body evidence bypasses the adapter and never qualifies adapter behavior.', 'Suite totals never infer per-tool positive evidence.', 'Failed live sessions are not verified.', 'Missing tests are Missing; refusals do not qualify success.') + suites = $suiteReports; liveSession = if ($LiveSummary) { [ordered]@{ summaryPath = [IO.Path]::GetFullPath($LiveSummary); summarySha256 = (Get-FileHash -LiteralPath $LiveSummary).Hash; receiptsPath = $liveReceipts.path; receiptsSha256 = $liveReceipts.sha256; passed = $livePassed; runId = $live['runId']; candidateIdentity = $live['selected_distribution']; host = $live['host'] } } else { $null } + tools = @($rows) +} +[IO.Directory]::CreateDirectory([IO.Path]::GetFullPath($OutputDirectory)) | Out-Null +$output = Join-Path $OutputDirectory 'tool-results.json' +$report | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $output -Encoding utf8 +[pscustomobject]@{ Report = [IO.Path]::GetFullPath($output); Tools = $rows.Count; RoutingComplete = $report.routingComplete; NativeQualificationComplete = $false } diff --git a/libs/CheatEngine.Mcp.Core/packages.lock.json b/libs/CheatEngine.Mcp.Core/packages.lock.json index 0eb0443..d0684ca 100644 --- a/libs/CheatEngine.Mcp.Core/packages.lock.json +++ b/libs/CheatEngine.Mcp.Core/packages.lock.json @@ -4,12 +4,12 @@ "net10.0": { "CheatEngine.Client": { "type": "Direct", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "nJ262v9o4Z2QXmG8+Ny5G0V850IYQIdL6GMnugvwPQiqyI3zZgOplU0HBSM+vX1B8k+geTXUFS53IZA03EtigA==", + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "7Gwflxn1fzSMgHFecIhnUwiOHHuM2ysBKa8e0okkr6S/b8Xv821vlGpJHA0LGT8rzhJ8Otbx5PH03//pfgrNVw==", "dependencies": { - "CheatEngine.Client.Fluent": "[1.0.0]", - "CheatEngine.Client.Hosting": "[1.0.0]" + "CheatEngine.Client.Fluent": "[1.1.0]", + "CheatEngine.Client.Hosting": "[1.1.0]" } }, "Microsoft.NET.ILLink.Tasks": { @@ -31,28 +31,28 @@ }, "CheatEngine.Client.Abstractions": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "1Ma1AEwpAZ2Q6c16ydNzOIsnchanixkH0VgSASOoRoeV+DPtTp98zfi4ZDLat/fkMXxPJNysCGRVRxm5hES+eg==", + "resolved": "1.1.0", + "contentHash": "CpaOmRG1KJ7f6QxWXowEp8ySvBbb7qTQWB12ulocau35SUYpz9Ug0R52zYoJc+nEeVcQhLCGp/nzuoIJHD9tfg==", "dependencies": { "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Core": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "MHYLqRPy1NGm8k5uyMB9PnazHwPCyQgs1qUYrei47M0gDUW3yChZJ6N7xrcsng4bJTiqUqIvfOIoPU3MCSr5VA==", + "resolved": "1.1.0", + "contentHash": "p3Av7agZlLZxr5fvwbW2Uq8TPwOT+PTzs/Hs92zcA8egpQUqyhnAzQOSWPJUNVk+TYDnXWj0nvQwcDMiaEfkTw==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "d7zKtwOIkBg7dyXLMP8Z9saHVRj1D82/wg4mAo6ds37ecVylVWzsGusNU2HXiN705pTbs6NRB7k8LEak/KR3Cw==", + "resolved": "1.1.0", + "contentHash": "ARBq9El8zUijcol/NS/mabxeWHVhPc8flDWQLnEIYoFo9nipLgPwG8SzZT+IckA0BqPtu6LPn3UZDsHReBYqSA==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", - "CheatEngine.Client.Core": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", + "CheatEngine.Client.Core": "[1.1.0]", "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", "Microsoft.Extensions.DependencyInjection": "10.0.12", "Microsoft.Extensions.Logging": "10.0.12", @@ -62,18 +62,18 @@ }, "CheatEngine.Client.Fluent": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "8/4JcuUjaG7iWHgpg6j+nt3r4xSfV5KyjlJ9NlMbX0SWKoRIbTXeUezQjmA0X5XfC76JOmWLBCPuPGbKHv4dmg==", + "resolved": "1.1.0", + "contentHash": "Q4baX9mQpyuGRO8x1ROOMidejY1o6I9Zkh2uCFEHMIkZK+bdzMas+yQzPyJt80jQUH0InAqCb1tnk4AjELRQZQ==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]" } }, "CheatEngine.Client.Hosting": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "NxpEXtZ6VN1URhR3mrD6uAqr7Os5zp2y7k45X1+2k0M68XBQC+HIobp9QUBb/q5d2tDgAlZxpHWuS6xhasqJTQ==", + "resolved": "1.1.0", + "contentHash": "atDiA6dpnP7Mo6dZ9G3XIou8rc2Vc9g5YKvYnps7hwzKP+pY8NUC6O3J8/IiyvN8G0QIZeL7QGSILt8AuCnqIg==", "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[1.0.0]", + "CheatEngine.Client.Extensions.DependencyInjection": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)", "Microsoft.Extensions.DependencyInjection": "10.0.12", "Microsoft.Extensions.Logging": "10.0.12" @@ -182,9 +182,9 @@ }, "CheatEngine.SDK": { "type": "CentralTransitive", - "requested": "[2.0.0, )", - "resolved": "2.0.0", - "contentHash": "NLEdZYJ9LKW3EFNB4X5snKCQf7ZS86GkCQ+El7o+S1XQcxHQGjS45Q1ap8lfjQuIwm004mQ3TPxo+ph1yvRrlQ==" + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "coRWNRz81E5N3PQeruDzHEDSeQQMSYkuCIx9LpKWO3qP61v6hbyRvYlHzjco2iNVykxy9z25zVbivUbxpA5dow==" }, "Microsoft.Extensions.AI.Abstractions": { "type": "CentralTransitive", diff --git a/srcs/CheatEngine.Mcp.Gateway/packages.aot.lock.json b/srcs/CheatEngine.Mcp.Gateway/packages.aot.lock.json index eb3a608..7c5ff07 100644 --- a/srcs/CheatEngine.Mcp.Gateway/packages.aot.lock.json +++ b/srcs/CheatEngine.Mcp.Gateway/packages.aot.lock.json @@ -16,44 +16,44 @@ }, "CheatEngine.Client.Abstractions": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "1Ma1AEwpAZ2Q6c16ydNzOIsnchanixkH0VgSASOoRoeV+DPtTp98zfi4ZDLat/fkMXxPJNysCGRVRxm5hES+eg==", + "resolved": "1.1.0", + "contentHash": "CpaOmRG1KJ7f6QxWXowEp8ySvBbb7qTQWB12ulocau35SUYpz9Ug0R52zYoJc+nEeVcQhLCGp/nzuoIJHD9tfg==", "dependencies": { "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Core": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "MHYLqRPy1NGm8k5uyMB9PnazHwPCyQgs1qUYrei47M0gDUW3yChZJ6N7xrcsng4bJTiqUqIvfOIoPU3MCSr5VA==", + "resolved": "1.1.0", + "contentHash": "p3Av7agZlLZxr5fvwbW2Uq8TPwOT+PTzs/Hs92zcA8egpQUqyhnAzQOSWPJUNVk+TYDnXWj0nvQwcDMiaEfkTw==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "d7zKtwOIkBg7dyXLMP8Z9saHVRj1D82/wg4mAo6ds37ecVylVWzsGusNU2HXiN705pTbs6NRB7k8LEak/KR3Cw==", + "resolved": "1.1.0", + "contentHash": "ARBq9El8zUijcol/NS/mabxeWHVhPc8flDWQLnEIYoFo9nipLgPwG8SzZT+IckA0BqPtu6LPn3UZDsHReBYqSA==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", - "CheatEngine.Client.Core": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]", + "CheatEngine.Client.Core": "[1.1.0]" } }, "CheatEngine.Client.Fluent": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "8/4JcuUjaG7iWHgpg6j+nt3r4xSfV5KyjlJ9NlMbX0SWKoRIbTXeUezQjmA0X5XfC76JOmWLBCPuPGbKHv4dmg==", + "resolved": "1.1.0", + "contentHash": "Q4baX9mQpyuGRO8x1ROOMidejY1o6I9Zkh2uCFEHMIkZK+bdzMas+yQzPyJt80jQUH0InAqCb1tnk4AjELRQZQ==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]" } }, "CheatEngine.Client.Hosting": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "NxpEXtZ6VN1URhR3mrD6uAqr7Os5zp2y7k45X1+2k0M68XBQC+HIobp9QUBb/q5d2tDgAlZxpHWuS6xhasqJTQ==", + "resolved": "1.1.0", + "contentHash": "atDiA6dpnP7Mo6dZ9G3XIou8rc2Vc9g5YKvYnps7hwzKP+pY8NUC6O3J8/IiyvN8G0QIZeL7QGSILt8AuCnqIg==", "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[1.0.0]", + "CheatEngine.Client.Extensions.DependencyInjection": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, @@ -68,52 +68,52 @@ "cheatengine.mcp.core": { "type": "Project", "dependencies": { - "CheatEngine.Client": "[1.0.0, )", + "CheatEngine.Client": "[1.1.0, )", "ModelContextProtocol": "[2.2.0, )" } }, "cheatengine.mcp.hosting": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0, )", + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", "ModelContextProtocol.AspNetCore": "[2.2.0, )" } }, "cheatengine.mcp.prompts": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0, )", - "CheatEngine.Mcp.Resources": "[2.0.0, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Resources": "[2.0.0-beta.4, )" } }, "cheatengine.mcp.resources": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0, )", - "CheatEngine.Mcp.Tools": "[2.0.0, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Tools": "[2.0.0-beta.4, )" } }, "cheatengine.mcp.tools": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )" } }, "CheatEngine.Client": { "type": "CentralTransitive", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "nJ262v9o4Z2QXmG8+Ny5G0V850IYQIdL6GMnugvwPQiqyI3zZgOplU0HBSM+vX1B8k+geTXUFS53IZA03EtigA==", + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "7Gwflxn1fzSMgHFecIhnUwiOHHuM2ysBKa8e0okkr6S/b8Xv821vlGpJHA0LGT8rzhJ8Otbx5PH03//pfgrNVw==", "dependencies": { - "CheatEngine.Client.Fluent": "[1.0.0]", - "CheatEngine.Client.Hosting": "[1.0.0]" + "CheatEngine.Client.Fluent": "[1.1.0]", + "CheatEngine.Client.Hosting": "[1.1.0]" } }, "CheatEngine.SDK": { "type": "CentralTransitive", - "requested": "[2.0.0, )", - "resolved": "2.0.0", - "contentHash": "NLEdZYJ9LKW3EFNB4X5snKCQf7ZS86GkCQ+El7o+S1XQcxHQGjS45Q1ap8lfjQuIwm004mQ3TPxo+ph1yvRrlQ==" + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "coRWNRz81E5N3PQeruDzHEDSeQQMSYkuCIx9LpKWO3qP61v6hbyRvYlHzjco2iNVykxy9z25zVbivUbxpA5dow==" }, "Microsoft.Extensions.AI.Abstractions": { "type": "CentralTransitive", diff --git a/srcs/CheatEngine.Mcp.Gateway/packages.lock.json b/srcs/CheatEngine.Mcp.Gateway/packages.lock.json index 79cde4e..92f7542 100644 --- a/srcs/CheatEngine.Mcp.Gateway/packages.lock.json +++ b/srcs/CheatEngine.Mcp.Gateway/packages.lock.json @@ -10,44 +10,44 @@ }, "CheatEngine.Client.Abstractions": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "1Ma1AEwpAZ2Q6c16ydNzOIsnchanixkH0VgSASOoRoeV+DPtTp98zfi4ZDLat/fkMXxPJNysCGRVRxm5hES+eg==", + "resolved": "1.1.0", + "contentHash": "CpaOmRG1KJ7f6QxWXowEp8ySvBbb7qTQWB12ulocau35SUYpz9Ug0R52zYoJc+nEeVcQhLCGp/nzuoIJHD9tfg==", "dependencies": { "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Core": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "MHYLqRPy1NGm8k5uyMB9PnazHwPCyQgs1qUYrei47M0gDUW3yChZJ6N7xrcsng4bJTiqUqIvfOIoPU3MCSr5VA==", + "resolved": "1.1.0", + "contentHash": "p3Av7agZlLZxr5fvwbW2Uq8TPwOT+PTzs/Hs92zcA8egpQUqyhnAzQOSWPJUNVk+TYDnXWj0nvQwcDMiaEfkTw==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "d7zKtwOIkBg7dyXLMP8Z9saHVRj1D82/wg4mAo6ds37ecVylVWzsGusNU2HXiN705pTbs6NRB7k8LEak/KR3Cw==", + "resolved": "1.1.0", + "contentHash": "ARBq9El8zUijcol/NS/mabxeWHVhPc8flDWQLnEIYoFo9nipLgPwG8SzZT+IckA0BqPtu6LPn3UZDsHReBYqSA==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", - "CheatEngine.Client.Core": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]", + "CheatEngine.Client.Core": "[1.1.0]" } }, "CheatEngine.Client.Fluent": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "8/4JcuUjaG7iWHgpg6j+nt3r4xSfV5KyjlJ9NlMbX0SWKoRIbTXeUezQjmA0X5XfC76JOmWLBCPuPGbKHv4dmg==", + "resolved": "1.1.0", + "contentHash": "Q4baX9mQpyuGRO8x1ROOMidejY1o6I9Zkh2uCFEHMIkZK+bdzMas+yQzPyJt80jQUH0InAqCb1tnk4AjELRQZQ==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]" } }, "CheatEngine.Client.Hosting": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "NxpEXtZ6VN1URhR3mrD6uAqr7Os5zp2y7k45X1+2k0M68XBQC+HIobp9QUBb/q5d2tDgAlZxpHWuS6xhasqJTQ==", + "resolved": "1.1.0", + "contentHash": "atDiA6dpnP7Mo6dZ9G3XIou8rc2Vc9g5YKvYnps7hwzKP+pY8NUC6O3J8/IiyvN8G0QIZeL7QGSILt8AuCnqIg==", "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[1.0.0]", + "CheatEngine.Client.Extensions.DependencyInjection": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, @@ -62,52 +62,52 @@ "cheatengine.mcp.core": { "type": "Project", "dependencies": { - "CheatEngine.Client": "[1.0.0, )", + "CheatEngine.Client": "[1.1.0, )", "ModelContextProtocol": "[2.2.0, )" } }, "cheatengine.mcp.hosting": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", "ModelContextProtocol.AspNetCore": "[2.2.0, )" } }, "cheatengine.mcp.prompts": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Resources": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Resources": "[2.0.0-beta.4, )" } }, "cheatengine.mcp.resources": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Tools": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Tools": "[2.0.0-beta.4, )" } }, "cheatengine.mcp.tools": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )" } }, "CheatEngine.Client": { "type": "CentralTransitive", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "nJ262v9o4Z2QXmG8+Ny5G0V850IYQIdL6GMnugvwPQiqyI3zZgOplU0HBSM+vX1B8k+geTXUFS53IZA03EtigA==", + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "7Gwflxn1fzSMgHFecIhnUwiOHHuM2ysBKa8e0okkr6S/b8Xv821vlGpJHA0LGT8rzhJ8Otbx5PH03//pfgrNVw==", "dependencies": { - "CheatEngine.Client.Fluent": "[1.0.0]", - "CheatEngine.Client.Hosting": "[1.0.0]" + "CheatEngine.Client.Fluent": "[1.1.0]", + "CheatEngine.Client.Hosting": "[1.1.0]" } }, "CheatEngine.SDK": { "type": "CentralTransitive", - "requested": "[2.0.0, )", - "resolved": "2.0.0", - "contentHash": "NLEdZYJ9LKW3EFNB4X5snKCQf7ZS86GkCQ+El7o+S1XQcxHQGjS45Q1ap8lfjQuIwm004mQ3TPxo+ph1yvRrlQ==" + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "coRWNRz81E5N3PQeruDzHEDSeQQMSYkuCIx9LpKWO3qP61v6hbyRvYlHzjco2iNVykxy9z25zVbivUbxpA5dow==" }, "Microsoft.Extensions.AI.Abstractions": { "type": "CentralTransitive", diff --git a/srcs/CheatEngine.Mcp.Hosting/packages.lock.json b/srcs/CheatEngine.Mcp.Hosting/packages.lock.json index c651d55..995788a 100644 --- a/srcs/CheatEngine.Mcp.Hosting/packages.lock.json +++ b/srcs/CheatEngine.Mcp.Hosting/packages.lock.json @@ -19,44 +19,44 @@ }, "CheatEngine.Client.Abstractions": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "1Ma1AEwpAZ2Q6c16ydNzOIsnchanixkH0VgSASOoRoeV+DPtTp98zfi4ZDLat/fkMXxPJNysCGRVRxm5hES+eg==", + "resolved": "1.1.0", + "contentHash": "CpaOmRG1KJ7f6QxWXowEp8ySvBbb7qTQWB12ulocau35SUYpz9Ug0R52zYoJc+nEeVcQhLCGp/nzuoIJHD9tfg==", "dependencies": { "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Core": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "MHYLqRPy1NGm8k5uyMB9PnazHwPCyQgs1qUYrei47M0gDUW3yChZJ6N7xrcsng4bJTiqUqIvfOIoPU3MCSr5VA==", + "resolved": "1.1.0", + "contentHash": "p3Av7agZlLZxr5fvwbW2Uq8TPwOT+PTzs/Hs92zcA8egpQUqyhnAzQOSWPJUNVk+TYDnXWj0nvQwcDMiaEfkTw==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "d7zKtwOIkBg7dyXLMP8Z9saHVRj1D82/wg4mAo6ds37ecVylVWzsGusNU2HXiN705pTbs6NRB7k8LEak/KR3Cw==", + "resolved": "1.1.0", + "contentHash": "ARBq9El8zUijcol/NS/mabxeWHVhPc8flDWQLnEIYoFo9nipLgPwG8SzZT+IckA0BqPtu6LPn3UZDsHReBYqSA==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", - "CheatEngine.Client.Core": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]", + "CheatEngine.Client.Core": "[1.1.0]" } }, "CheatEngine.Client.Fluent": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "8/4JcuUjaG7iWHgpg6j+nt3r4xSfV5KyjlJ9NlMbX0SWKoRIbTXeUezQjmA0X5XfC76JOmWLBCPuPGbKHv4dmg==", + "resolved": "1.1.0", + "contentHash": "Q4baX9mQpyuGRO8x1ROOMidejY1o6I9Zkh2uCFEHMIkZK+bdzMas+yQzPyJt80jQUH0InAqCb1tnk4AjELRQZQ==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]" } }, "CheatEngine.Client.Hosting": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "NxpEXtZ6VN1URhR3mrD6uAqr7Os5zp2y7k45X1+2k0M68XBQC+HIobp9QUBb/q5d2tDgAlZxpHWuS6xhasqJTQ==", + "resolved": "1.1.0", + "contentHash": "atDiA6dpnP7Mo6dZ9G3XIou8rc2Vc9g5YKvYnps7hwzKP+pY8NUC6O3J8/IiyvN8G0QIZeL7QGSILt8AuCnqIg==", "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[1.0.0]", + "CheatEngine.Client.Extensions.DependencyInjection": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, @@ -71,25 +71,25 @@ "cheatengine.mcp.core": { "type": "Project", "dependencies": { - "CheatEngine.Client": "[1.0.0, )", + "CheatEngine.Client": "[1.1.0, )", "ModelContextProtocol": "[2.2.0, )" } }, "CheatEngine.Client": { "type": "CentralTransitive", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "nJ262v9o4Z2QXmG8+Ny5G0V850IYQIdL6GMnugvwPQiqyI3zZgOplU0HBSM+vX1B8k+geTXUFS53IZA03EtigA==", + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "7Gwflxn1fzSMgHFecIhnUwiOHHuM2ysBKa8e0okkr6S/b8Xv821vlGpJHA0LGT8rzhJ8Otbx5PH03//pfgrNVw==", "dependencies": { - "CheatEngine.Client.Fluent": "[1.0.0]", - "CheatEngine.Client.Hosting": "[1.0.0]" + "CheatEngine.Client.Fluent": "[1.1.0]", + "CheatEngine.Client.Hosting": "[1.1.0]" } }, "CheatEngine.SDK": { "type": "CentralTransitive", - "requested": "[2.0.0, )", - "resolved": "2.0.0", - "contentHash": "NLEdZYJ9LKW3EFNB4X5snKCQf7ZS86GkCQ+El7o+S1XQcxHQGjS45Q1ap8lfjQuIwm004mQ3TPxo+ph1yvRrlQ==" + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "coRWNRz81E5N3PQeruDzHEDSeQQMSYkuCIx9LpKWO3qP61v6hbyRvYlHzjco2iNVykxy9z25zVbivUbxpA5dow==" }, "Microsoft.Extensions.AI.Abstractions": { "type": "CentralTransitive", diff --git a/srcs/CheatEngine.Mcp.Plugin/packages.lock.json b/srcs/CheatEngine.Mcp.Plugin/packages.lock.json index 3a1d921..efaa30c 100644 --- a/srcs/CheatEngine.Mcp.Plugin/packages.lock.json +++ b/srcs/CheatEngine.Mcp.Plugin/packages.lock.json @@ -4,19 +4,19 @@ "net10.0": { "CheatEngine.Client": { "type": "Direct", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "nJ262v9o4Z2QXmG8+Ny5G0V850IYQIdL6GMnugvwPQiqyI3zZgOplU0HBSM+vX1B8k+geTXUFS53IZA03EtigA==", + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "7Gwflxn1fzSMgHFecIhnUwiOHHuM2ysBKa8e0okkr6S/b8Xv821vlGpJHA0LGT8rzhJ8Otbx5PH03//pfgrNVw==", "dependencies": { - "CheatEngine.Client.Fluent": "[1.0.0]", - "CheatEngine.Client.Hosting": "[1.0.0]" + "CheatEngine.Client.Fluent": "[1.1.0]", + "CheatEngine.Client.Hosting": "[1.1.0]" } }, "CheatEngine.SDK": { "type": "Direct", - "requested": "[2.0.0, )", - "resolved": "2.0.0", - "contentHash": "NLEdZYJ9LKW3EFNB4X5snKCQf7ZS86GkCQ+El7o+S1XQcxHQGjS45Q1ap8lfjQuIwm004mQ3TPxo+ph1yvRrlQ==" + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "coRWNRz81E5N3PQeruDzHEDSeQQMSYkuCIx9LpKWO3qP61v6hbyRvYlHzjco2iNVykxy9z25zVbivUbxpA5dow==" }, "Costura.Fody": { "type": "Direct", @@ -50,44 +50,44 @@ }, "CheatEngine.Client.Abstractions": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "1Ma1AEwpAZ2Q6c16ydNzOIsnchanixkH0VgSASOoRoeV+DPtTp98zfi4ZDLat/fkMXxPJNysCGRVRxm5hES+eg==", + "resolved": "1.1.0", + "contentHash": "CpaOmRG1KJ7f6QxWXowEp8ySvBbb7qTQWB12ulocau35SUYpz9Ug0R52zYoJc+nEeVcQhLCGp/nzuoIJHD9tfg==", "dependencies": { "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Core": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "MHYLqRPy1NGm8k5uyMB9PnazHwPCyQgs1qUYrei47M0gDUW3yChZJ6N7xrcsng4bJTiqUqIvfOIoPU3MCSr5VA==", + "resolved": "1.1.0", + "contentHash": "p3Av7agZlLZxr5fvwbW2Uq8TPwOT+PTzs/Hs92zcA8egpQUqyhnAzQOSWPJUNVk+TYDnXWj0nvQwcDMiaEfkTw==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "d7zKtwOIkBg7dyXLMP8Z9saHVRj1D82/wg4mAo6ds37ecVylVWzsGusNU2HXiN705pTbs6NRB7k8LEak/KR3Cw==", + "resolved": "1.1.0", + "contentHash": "ARBq9El8zUijcol/NS/mabxeWHVhPc8flDWQLnEIYoFo9nipLgPwG8SzZT+IckA0BqPtu6LPn3UZDsHReBYqSA==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", - "CheatEngine.Client.Core": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]", + "CheatEngine.Client.Core": "[1.1.0]" } }, "CheatEngine.Client.Fluent": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "8/4JcuUjaG7iWHgpg6j+nt3r4xSfV5KyjlJ9NlMbX0SWKoRIbTXeUezQjmA0X5XfC76JOmWLBCPuPGbKHv4dmg==", + "resolved": "1.1.0", + "contentHash": "Q4baX9mQpyuGRO8x1ROOMidejY1o6I9Zkh2uCFEHMIkZK+bdzMas+yQzPyJt80jQUH0InAqCb1tnk4AjELRQZQ==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]" } }, "CheatEngine.Client.Hosting": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "NxpEXtZ6VN1URhR3mrD6uAqr7Os5zp2y7k45X1+2k0M68XBQC+HIobp9QUBb/q5d2tDgAlZxpHWuS6xhasqJTQ==", + "resolved": "1.1.0", + "contentHash": "atDiA6dpnP7Mo6dZ9G3XIou8rc2Vc9g5YKvYnps7hwzKP+pY8NUC6O3J8/IiyvN8G0QIZeL7QGSILt8AuCnqIg==", "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[1.0.0]", + "CheatEngine.Client.Extensions.DependencyInjection": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, @@ -102,35 +102,35 @@ "cheatengine.mcp.core": { "type": "Project", "dependencies": { - "CheatEngine.Client": "[1.0.0, )", + "CheatEngine.Client": "[1.1.0, )", "ModelContextProtocol": "[2.2.0, )" } }, "cheatengine.mcp.hosting": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", "ModelContextProtocol.AspNetCore": "[2.2.0, )" } }, "cheatengine.mcp.prompts": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Resources": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Resources": "[2.0.0-beta.4, )" } }, "cheatengine.mcp.resources": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Tools": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Tools": "[2.0.0-beta.4, )" } }, "cheatengine.mcp.tools": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )" } }, "Microsoft.Extensions.AI.Abstractions": { diff --git a/srcs/CheatEngine.Mcp.Prompts/packages.lock.json b/srcs/CheatEngine.Mcp.Prompts/packages.lock.json index 0daca87..209df83 100644 --- a/srcs/CheatEngine.Mcp.Prompts/packages.lock.json +++ b/srcs/CheatEngine.Mcp.Prompts/packages.lock.json @@ -10,28 +10,28 @@ }, "CheatEngine.Client.Abstractions": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "1Ma1AEwpAZ2Q6c16ydNzOIsnchanixkH0VgSASOoRoeV+DPtTp98zfi4ZDLat/fkMXxPJNysCGRVRxm5hES+eg==", + "resolved": "1.1.0", + "contentHash": "CpaOmRG1KJ7f6QxWXowEp8ySvBbb7qTQWB12ulocau35SUYpz9Ug0R52zYoJc+nEeVcQhLCGp/nzuoIJHD9tfg==", "dependencies": { "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Core": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "MHYLqRPy1NGm8k5uyMB9PnazHwPCyQgs1qUYrei47M0gDUW3yChZJ6N7xrcsng4bJTiqUqIvfOIoPU3MCSr5VA==", + "resolved": "1.1.0", + "contentHash": "p3Av7agZlLZxr5fvwbW2Uq8TPwOT+PTzs/Hs92zcA8egpQUqyhnAzQOSWPJUNVk+TYDnXWj0nvQwcDMiaEfkTw==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "d7zKtwOIkBg7dyXLMP8Z9saHVRj1D82/wg4mAo6ds37ecVylVWzsGusNU2HXiN705pTbs6NRB7k8LEak/KR3Cw==", + "resolved": "1.1.0", + "contentHash": "ARBq9El8zUijcol/NS/mabxeWHVhPc8flDWQLnEIYoFo9nipLgPwG8SzZT+IckA0BqPtu6LPn3UZDsHReBYqSA==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", - "CheatEngine.Client.Core": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", + "CheatEngine.Client.Core": "[1.1.0]", "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", "Microsoft.Extensions.DependencyInjection": "10.0.12", "Microsoft.Extensions.Logging": "10.0.12", @@ -41,18 +41,18 @@ }, "CheatEngine.Client.Fluent": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "8/4JcuUjaG7iWHgpg6j+nt3r4xSfV5KyjlJ9NlMbX0SWKoRIbTXeUezQjmA0X5XfC76JOmWLBCPuPGbKHv4dmg==", + "resolved": "1.1.0", + "contentHash": "Q4baX9mQpyuGRO8x1ROOMidejY1o6I9Zkh2uCFEHMIkZK+bdzMas+yQzPyJt80jQUH0InAqCb1tnk4AjELRQZQ==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]" } }, "CheatEngine.Client.Hosting": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "NxpEXtZ6VN1URhR3mrD6uAqr7Os5zp2y7k45X1+2k0M68XBQC+HIobp9QUBb/q5d2tDgAlZxpHWuS6xhasqJTQ==", + "resolved": "1.1.0", + "contentHash": "atDiA6dpnP7Mo6dZ9G3XIou8rc2Vc9g5YKvYnps7hwzKP+pY8NUC6O3J8/IiyvN8G0QIZeL7QGSILt8AuCnqIg==", "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[1.0.0]", + "CheatEngine.Client.Extensions.DependencyInjection": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)", "Microsoft.Extensions.DependencyInjection": "10.0.12", "Microsoft.Extensions.Logging": "10.0.12" @@ -162,38 +162,38 @@ "cheatengine.mcp.core": { "type": "Project", "dependencies": { - "CheatEngine.Client": "[1.0.0, )", + "CheatEngine.Client": "[1.1.0, )", "ModelContextProtocol": "[2.2.0, )" } }, "cheatengine.mcp.resources": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Tools": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Tools": "[2.0.0-beta.4, )" } }, "cheatengine.mcp.tools": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )" } }, "CheatEngine.Client": { "type": "CentralTransitive", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "nJ262v9o4Z2QXmG8+Ny5G0V850IYQIdL6GMnugvwPQiqyI3zZgOplU0HBSM+vX1B8k+geTXUFS53IZA03EtigA==", + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "7Gwflxn1fzSMgHFecIhnUwiOHHuM2ysBKa8e0okkr6S/b8Xv821vlGpJHA0LGT8rzhJ8Otbx5PH03//pfgrNVw==", "dependencies": { - "CheatEngine.Client.Fluent": "[1.0.0]", - "CheatEngine.Client.Hosting": "[1.0.0]" + "CheatEngine.Client.Fluent": "[1.1.0]", + "CheatEngine.Client.Hosting": "[1.1.0]" } }, "CheatEngine.SDK": { "type": "CentralTransitive", - "requested": "[2.0.0, )", - "resolved": "2.0.0", - "contentHash": "NLEdZYJ9LKW3EFNB4X5snKCQf7ZS86GkCQ+El7o+S1XQcxHQGjS45Q1ap8lfjQuIwm004mQ3TPxo+ph1yvRrlQ==" + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "coRWNRz81E5N3PQeruDzHEDSeQQMSYkuCIx9LpKWO3qP61v6hbyRvYlHzjco2iNVykxy9z25zVbivUbxpA5dow==" }, "Microsoft.Extensions.AI.Abstractions": { "type": "CentralTransitive", diff --git a/srcs/CheatEngine.Mcp.Resources/packages.lock.json b/srcs/CheatEngine.Mcp.Resources/packages.lock.json index 7955de0..fe1f66d 100644 --- a/srcs/CheatEngine.Mcp.Resources/packages.lock.json +++ b/srcs/CheatEngine.Mcp.Resources/packages.lock.json @@ -10,28 +10,28 @@ }, "CheatEngine.Client.Abstractions": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "1Ma1AEwpAZ2Q6c16ydNzOIsnchanixkH0VgSASOoRoeV+DPtTp98zfi4ZDLat/fkMXxPJNysCGRVRxm5hES+eg==", + "resolved": "1.1.0", + "contentHash": "CpaOmRG1KJ7f6QxWXowEp8ySvBbb7qTQWB12ulocau35SUYpz9Ug0R52zYoJc+nEeVcQhLCGp/nzuoIJHD9tfg==", "dependencies": { "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Core": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "MHYLqRPy1NGm8k5uyMB9PnazHwPCyQgs1qUYrei47M0gDUW3yChZJ6N7xrcsng4bJTiqUqIvfOIoPU3MCSr5VA==", + "resolved": "1.1.0", + "contentHash": "p3Av7agZlLZxr5fvwbW2Uq8TPwOT+PTzs/Hs92zcA8egpQUqyhnAzQOSWPJUNVk+TYDnXWj0nvQwcDMiaEfkTw==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "d7zKtwOIkBg7dyXLMP8Z9saHVRj1D82/wg4mAo6ds37ecVylVWzsGusNU2HXiN705pTbs6NRB7k8LEak/KR3Cw==", + "resolved": "1.1.0", + "contentHash": "ARBq9El8zUijcol/NS/mabxeWHVhPc8flDWQLnEIYoFo9nipLgPwG8SzZT+IckA0BqPtu6LPn3UZDsHReBYqSA==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", - "CheatEngine.Client.Core": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", + "CheatEngine.Client.Core": "[1.1.0]", "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", "Microsoft.Extensions.DependencyInjection": "10.0.12", "Microsoft.Extensions.Logging": "10.0.12", @@ -41,18 +41,18 @@ }, "CheatEngine.Client.Fluent": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "8/4JcuUjaG7iWHgpg6j+nt3r4xSfV5KyjlJ9NlMbX0SWKoRIbTXeUezQjmA0X5XfC76JOmWLBCPuPGbKHv4dmg==", + "resolved": "1.1.0", + "contentHash": "Q4baX9mQpyuGRO8x1ROOMidejY1o6I9Zkh2uCFEHMIkZK+bdzMas+yQzPyJt80jQUH0InAqCb1tnk4AjELRQZQ==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]" } }, "CheatEngine.Client.Hosting": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "NxpEXtZ6VN1URhR3mrD6uAqr7Os5zp2y7k45X1+2k0M68XBQC+HIobp9QUBb/q5d2tDgAlZxpHWuS6xhasqJTQ==", + "resolved": "1.1.0", + "contentHash": "atDiA6dpnP7Mo6dZ9G3XIou8rc2Vc9g5YKvYnps7hwzKP+pY8NUC6O3J8/IiyvN8G0QIZeL7QGSILt8AuCnqIg==", "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[1.0.0]", + "CheatEngine.Client.Extensions.DependencyInjection": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)", "Microsoft.Extensions.DependencyInjection": "10.0.12", "Microsoft.Extensions.Logging": "10.0.12" @@ -162,31 +162,31 @@ "cheatengine.mcp.core": { "type": "Project", "dependencies": { - "CheatEngine.Client": "[1.0.0, )", + "CheatEngine.Client": "[1.1.0, )", "ModelContextProtocol": "[2.2.0, )" } }, "cheatengine.mcp.tools": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )" } }, "CheatEngine.Client": { "type": "CentralTransitive", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "nJ262v9o4Z2QXmG8+Ny5G0V850IYQIdL6GMnugvwPQiqyI3zZgOplU0HBSM+vX1B8k+geTXUFS53IZA03EtigA==", + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "7Gwflxn1fzSMgHFecIhnUwiOHHuM2ysBKa8e0okkr6S/b8Xv821vlGpJHA0LGT8rzhJ8Otbx5PH03//pfgrNVw==", "dependencies": { - "CheatEngine.Client.Fluent": "[1.0.0]", - "CheatEngine.Client.Hosting": "[1.0.0]" + "CheatEngine.Client.Fluent": "[1.1.0]", + "CheatEngine.Client.Hosting": "[1.1.0]" } }, "CheatEngine.SDK": { "type": "CentralTransitive", - "requested": "[2.0.0, )", - "resolved": "2.0.0", - "contentHash": "NLEdZYJ9LKW3EFNB4X5snKCQf7ZS86GkCQ+El7o+S1XQcxHQGjS45Q1ap8lfjQuIwm004mQ3TPxo+ph1yvRrlQ==" + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "coRWNRz81E5N3PQeruDzHEDSeQQMSYkuCIx9LpKWO3qP61v6hbyRvYlHzjco2iNVykxy9z25zVbivUbxpA5dow==" }, "Microsoft.Extensions.AI.Abstractions": { "type": "CentralTransitive", diff --git a/srcs/CheatEngine.Mcp.Tools/Code/CodeTools.cs b/srcs/CheatEngine.Mcp.Tools/Code/CodeTools.cs index 84b6ab6..8840808 100644 --- a/srcs/CheatEngine.Mcp.Tools/Code/CodeTools.cs +++ b/srcs/CheatEngine.Mcp.Tools/Code/CodeTools.cs @@ -388,7 +388,7 @@ public CodeComments GetComments( } CodeLuaComments comments = _dispatch.RunLua(CheatEngineToolNames.CodeGetComments, CodeScripts.GetComments, - CodeLuaJsonContext.Default.CodeLuaComments, cancellationToken, expressions); + CodeLuaJsonContext.Default.CodeLuaComments, cancellationToken, (object) expressions); return new CodeComments([ .. comments.Comments.Select(comment => new CodeComment(comment.Address, comment.Comment)) ]); diff --git a/srcs/CheatEngine.Mcp.Tools/Processes/ProcessTools.cs b/srcs/CheatEngine.Mcp.Tools/Processes/ProcessTools.cs index 5da7588..2739be5 100644 --- a/srcs/CheatEngine.Mcp.Tools/Processes/ProcessTools.cs +++ b/srcs/CheatEngine.Mcp.Tools/Processes/ProcessTools.cs @@ -51,7 +51,7 @@ public sealed class ProcessTools local threads = {} local maximum = a[1] for i = 0, math.min(count, maximum) - 1 do - local threadId = tonumber(list.Strings[i], 16) + local threadId = tonumber(list[i], 16) assert(threadId ~= nil, 'getThreadlist returned an invalid hexadecimal thread id.') threads[#threads + 1] = threadId end diff --git a/srcs/CheatEngine.Mcp.Tools/packages.lock.json b/srcs/CheatEngine.Mcp.Tools/packages.lock.json index 964bc4b..ce83dd6 100644 --- a/srcs/CheatEngine.Mcp.Tools/packages.lock.json +++ b/srcs/CheatEngine.Mcp.Tools/packages.lock.json @@ -10,28 +10,28 @@ }, "CheatEngine.Client.Abstractions": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "1Ma1AEwpAZ2Q6c16ydNzOIsnchanixkH0VgSASOoRoeV+DPtTp98zfi4ZDLat/fkMXxPJNysCGRVRxm5hES+eg==", + "resolved": "1.1.0", + "contentHash": "CpaOmRG1KJ7f6QxWXowEp8ySvBbb7qTQWB12ulocau35SUYpz9Ug0R52zYoJc+nEeVcQhLCGp/nzuoIJHD9tfg==", "dependencies": { "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Core": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "MHYLqRPy1NGm8k5uyMB9PnazHwPCyQgs1qUYrei47M0gDUW3yChZJ6N7xrcsng4bJTiqUqIvfOIoPU3MCSr5VA==", + "resolved": "1.1.0", + "contentHash": "p3Av7agZlLZxr5fvwbW2Uq8TPwOT+PTzs/Hs92zcA8egpQUqyhnAzQOSWPJUNVk+TYDnXWj0nvQwcDMiaEfkTw==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "d7zKtwOIkBg7dyXLMP8Z9saHVRj1D82/wg4mAo6ds37ecVylVWzsGusNU2HXiN705pTbs6NRB7k8LEak/KR3Cw==", + "resolved": "1.1.0", + "contentHash": "ARBq9El8zUijcol/NS/mabxeWHVhPc8flDWQLnEIYoFo9nipLgPwG8SzZT+IckA0BqPtu6LPn3UZDsHReBYqSA==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", - "CheatEngine.Client.Core": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", + "CheatEngine.Client.Core": "[1.1.0]", "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", "Microsoft.Extensions.DependencyInjection": "10.0.12", "Microsoft.Extensions.Logging": "10.0.12", @@ -41,18 +41,18 @@ }, "CheatEngine.Client.Fluent": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "8/4JcuUjaG7iWHgpg6j+nt3r4xSfV5KyjlJ9NlMbX0SWKoRIbTXeUezQjmA0X5XfC76JOmWLBCPuPGbKHv4dmg==", + "resolved": "1.1.0", + "contentHash": "Q4baX9mQpyuGRO8x1ROOMidejY1o6I9Zkh2uCFEHMIkZK+bdzMas+yQzPyJt80jQUH0InAqCb1tnk4AjELRQZQ==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]" } }, "CheatEngine.Client.Hosting": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "NxpEXtZ6VN1URhR3mrD6uAqr7Os5zp2y7k45X1+2k0M68XBQC+HIobp9QUBb/q5d2tDgAlZxpHWuS6xhasqJTQ==", + "resolved": "1.1.0", + "contentHash": "atDiA6dpnP7Mo6dZ9G3XIou8rc2Vc9g5YKvYnps7hwzKP+pY8NUC6O3J8/IiyvN8G0QIZeL7QGSILt8AuCnqIg==", "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[1.0.0]", + "CheatEngine.Client.Extensions.DependencyInjection": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)", "Microsoft.Extensions.DependencyInjection": "10.0.12", "Microsoft.Extensions.Logging": "10.0.12" @@ -162,25 +162,25 @@ "cheatengine.mcp.core": { "type": "Project", "dependencies": { - "CheatEngine.Client": "[1.0.0, )", + "CheatEngine.Client": "[1.1.0, )", "ModelContextProtocol": "[2.2.0, )" } }, "CheatEngine.Client": { "type": "CentralTransitive", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "nJ262v9o4Z2QXmG8+Ny5G0V850IYQIdL6GMnugvwPQiqyI3zZgOplU0HBSM+vX1B8k+geTXUFS53IZA03EtigA==", + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "7Gwflxn1fzSMgHFecIhnUwiOHHuM2ysBKa8e0okkr6S/b8Xv821vlGpJHA0LGT8rzhJ8Otbx5PH03//pfgrNVw==", "dependencies": { - "CheatEngine.Client.Fluent": "[1.0.0]", - "CheatEngine.Client.Hosting": "[1.0.0]" + "CheatEngine.Client.Fluent": "[1.1.0]", + "CheatEngine.Client.Hosting": "[1.1.0]" } }, "CheatEngine.SDK": { "type": "CentralTransitive", - "requested": "[2.0.0, )", - "resolved": "2.0.0", - "contentHash": "NLEdZYJ9LKW3EFNB4X5snKCQf7ZS86GkCQ+El7o+S1XQcxHQGjS45Q1ap8lfjQuIwm004mQ3TPxo+ph1yvRrlQ==" + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "coRWNRz81E5N3PQeruDzHEDSeQQMSYkuCIx9LpKWO3qP61v6hbyRvYlHzjco2iNVykxy9z25zVbivUbxpA5dow==" }, "Microsoft.Extensions.AI.Abstractions": { "type": "CentralTransitive", diff --git a/tests/CheatEngine.Mcp.Tests/Contract/AllToolBehaviorEvidenceTests.cs b/tests/CheatEngine.Mcp.Tests/Contract/AllToolBehaviorEvidenceTests.cs new file mode 100644 index 0000000..f8d098a --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/Contract/AllToolBehaviorEvidenceTests.cs @@ -0,0 +1,73 @@ +using System.Reflection; +using System.Text.Json; + +using CheatEngine.Mcp.Core.Contract; +using CheatEngine.Mcp.Tests.Support; + +namespace CheatEngine.Mcp.Tests.Contract; + +/// New or renamed tools must retain reviewed, executable positive adapter evidence. +public sealed class AllToolBehaviorEvidenceTests +{ + private static readonly Dictionary Evidence = ReadEvidence(); + private static readonly string[] Suites = ["portable", "nativeLua"]; + + public static IEnumerable BackendTools => + McpPrimitiveCatalog.Create(TestComposition.BackendManifest).Tools + .Select(static tool => new object[] { tool.Name }); + + [Fact] + public void BehaviorEvidence_NamesMatchTheProductionBackendCatalogExactly() + { + string[] expected = McpPrimitiveCatalog.Create(TestComposition.BackendManifest).Tools + .Select(static tool => tool.Name).Order(StringComparer.Ordinal).ToArray(); + + Assert.NotEmpty(expected); + Assert.Equal(expected, Evidence.Keys.Order(StringComparer.Ordinal)); + } + + [Theory] + [MemberData(nameof(BackendTools))] + public void BackendTool_HasReviewedAdapterEvidenceReferencingExistingTests(string toolName) + { + Assert.True(Evidence.TryGetValue(toolName, out JsonElement tool), + $"{toolName} needs a reviewed positive adapter case in behavior-evidence.json."); + JsonElement[] adapters = tool.GetProperty("cases").EnumerateArray() + .Where(static entry => entry.GetProperty("scope").GetString() == "adapter").ToArray(); + Assert.NotEmpty(adapters); + foreach (JsonElement entry in adapters) + { + string methodName = entry.GetProperty("testMethod").GetString()!; + int separator = methodName.LastIndexOf('.'); + Assert.True(separator > 0, $"{toolName} has an invalid test method: {methodName}"); + Type? type = typeof(AllToolBehaviorEvidenceTests).Assembly.GetType(methodName[..separator]); + Assert.NotNull(type); + MethodInfo? method = type.GetMethod(methodName[(separator + 1)..], BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(method); + Assert.Contains(method.GetCustomAttributes(), static attribute => attribute is FactAttribute); + Assert.False(string.IsNullOrWhiteSpace(entry.GetProperty("oracle").GetString())); + Assert.Contains(entry.GetProperty("suite").GetString(), Suites); + + string source = Path.GetFullPath(Path.Combine(RepositoryPaths.Root, entry.GetProperty("sourceFile").GetString()!)); + Assert.StartsWith(Path.TrimEndingDirectorySeparator(RepositoryPaths.Root) + Path.DirectorySeparatorChar, + source, StringComparison.OrdinalIgnoreCase); + Assert.True(File.Exists(source), $"{toolName} references a missing test source: {source}"); + + string[] required = entry.GetProperty("requiredCases").EnumerateArray() + .Select(static item => item.GetString()!).ToArray(); + Assert.NotEmpty(required); + Assert.Equal(required.Length, required.Distinct(StringComparer.Ordinal).Count()); + Assert.All(required, name => Assert.True(name == methodName || + name.StartsWith(methodName + "(", StringComparison.Ordinal), + $"{toolName} has a required case outside its mapped test: {name}")); + } + } + + private static Dictionary ReadEvidence() + { + using JsonDocument document = JsonDocument.Parse(File.ReadAllText(Path.Combine(RepositoryPaths.Root, + "docs", "qualification", "all-tools", "behavior-evidence.json"))); + return document.RootElement.GetProperty("tools").EnumerateArray() + .ToDictionary(static tool => tool.GetProperty("name").GetString()!, static tool => tool.Clone(), StringComparer.Ordinal); + } +} diff --git a/tests/CheatEngine.Mcp.Tests/Hosting/AllToolGatewayQualificationTests.cs b/tests/CheatEngine.Mcp.Tests/Hosting/AllToolGatewayQualificationTests.cs new file mode 100644 index 0000000..71a33a3 --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/Hosting/AllToolGatewayQualificationTests.cs @@ -0,0 +1,263 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +using CheatEngine.Mcp.Core.Contract; +using CheatEngine.Mcp.Tests.Support; + +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; + +namespace CheatEngine.Mcp.Tests.Hosting; + +/// +/// Gateway-only qualification for every composed backend tool. The loopback backends deliberately echo requests; +/// these cases prove routing semantics, not a Cheat Engine tool's native behavior. +/// +[Collection(nameof(SerialTestGroup))] +public sealed class AllToolGatewayQualificationTests(AllToolGatewayFixture fixture) : IClassFixture +{ + private static CancellationToken Token => TestContext.Current.CancellationToken; + + /// The data source is the production backend catalog, so every catalog addition gets these routing cases. + public static IEnumerable BackendTools => BackendToolNames().Select(static name => new object[] { name }); + + [Fact] + public void BackendToolCases_AreCatalogDerivedAndCoverTheCurrentBackendSurface() + { + string[] names = BackendToolNames(); + + Assert.Equal(193, names.Length); + Assert.Equal(names.Distinct(StringComparer.Ordinal).Count(), names.Length); + Assert.Equal(names, TestComposition.GatewayTools.Skip(1).Select(static tool => tool.Name)); + } + + [Theory] + [MemberData(nameof(BackendTools))] + public async Task CallTool_MissingInstanceId_RejectsEveryBackendToolBeforeAnyBackendContact(string tool) + { + BackendCounts firstBefore = BackendCounts.Capture(fixture.First); + BackendCounts secondBefore = BackendCounts.Capture(fixture.Second); + await using McpClient client = await fixture.Gateway.ConnectAsync(); + + CallToolResult result = await client.CallToolAsync(tool, new Dictionary(), cancellationToken: Token); + + AssertError(result, tool, ToolErrorKind.InvalidArgument); + AssertUnchanged(fixture.First, firstBefore); + AssertUnchanged(fixture.Second, secondBefore); + } + + [Theory] + [MemberData(nameof(BackendTools))] + public async Task CallTool_UnknownInstanceId_RejectsEveryBackendToolWithoutFallback(string tool) + { + BackendCounts firstBefore = BackendCounts.Capture(fixture.First); + BackendCounts secondBefore = BackendCounts.Capture(fixture.Second); + await using McpClient client = await fixture.Gateway.ConnectAsync(); + + CallToolResult result = await client.CallToolAsync(tool, + new Dictionary + { + [GatewayToolCatalog.InstanceIdArgumentName] = "ce-404-00000000000000000000000000000000" + }, cancellationToken: Token); + + AssertError(result, tool, ToolErrorKind.InstanceUnavailable); + AssertUnchanged(fixture.First, firstBefore); + AssertUnchanged(fixture.Second, secondBefore); + } + + [Theory] + [MemberData(nameof(BackendTools))] + public async Task CallTool_StaleIdentity_RejectsEveryBackendToolBeforeMutation(string tool) + { + BackendCounts firstBefore = BackendCounts.Capture(fixture.First); + BackendCounts secondBefore = BackendCounts.Capture(fixture.Second); + fixture.First.ReportedIdentity = fixture.First.Descriptor with + { + ActivationId = Guid.NewGuid() + }; + try + { + await using McpClient client = await fixture.Gateway.ConnectAsync(); + CallToolResult result = await client.CallToolAsync(tool, RoutedArguments(fixture.First, tool, "stale"), cancellationToken: Token); + + AssertError(result, tool, ToolErrorKind.InstanceUnavailable); + Assert.Equal(firstBefore.ForwardedCalls, fixture.First.ForwardedCallCount); + Assert.Equal(firstBefore.InitializeCalls, fixture.First.InitializeCount); + Assert.Equal(firstBefore.IdentityProbes + 1, fixture.First.IdentityProbeCount); + AssertUnchanged(fixture.Second, secondBefore); + } + finally + { + fixture.First.ReportedIdentity = fixture.First.Descriptor; + } + } + + [Theory] + [MemberData(nameof(BackendTools))] + public async Task CallTool_TwoInstances_RoutesEveryBackendToolWithOnlyInstanceIdRemoved(string tool) + { + BackendCounts firstBefore = BackendCounts.Capture(fixture.First); + BackendCounts secondBefore = BackendCounts.Capture(fixture.Second); + await using McpClient client = await fixture.Gateway.ConnectAsync(); + + CallToolResult first = await client.CallToolAsync(tool, RoutedArguments(fixture.First, tool, "first"), cancellationToken: Token); + AssertForwarded(tool, fixture.First, "first", first); + Assert.Equal(firstBefore.ForwardedCalls + 1, fixture.First.ForwardedCallCount); + Assert.Equal(firstBefore.InitializeCalls + (firstBefore.InitializeCalls == 0 ? 1 : 0), + fixture.First.InitializeCount); + Assert.Equal(firstBefore.IdentityProbes + 1, fixture.First.IdentityProbeCount); + AssertUnchanged(fixture.Second, secondBefore); + + CallToolResult second = await client.CallToolAsync(tool, RoutedArguments(fixture.Second, tool, "second"), cancellationToken: Token); + AssertForwarded(tool, fixture.Second, "second", second); + Assert.Equal(secondBefore.ForwardedCalls + 1, fixture.Second.ForwardedCallCount); + Assert.Equal(secondBefore.InitializeCalls + (secondBefore.InitializeCalls == 0 ? 1 : 0), + fixture.Second.InitializeCount); + Assert.Equal(secondBefore.IdentityProbes + 1, fixture.Second.IdentityProbeCount); + } + + [Fact] + public async Task InstanceList_EmptyRegistry_ReturnsLocalEmptyDiscoveryResult() + { + await using GatewayTestHost gateway = await GatewayTestHost.StartAsync(); + await using McpClient client = await gateway.ConnectAsync(); + + CallToolResult result = await client.CallToolAsync(GatewayToolCatalog.InstanceListToolName, + new Dictionary(), cancellationToken: Token); + + Assert.NotEqual(true, result.IsError); + JsonObject payload = StructuredPayload(result); + Assert.Empty(payload["instances"]!.AsArray()); + Assert.False(payload["discoveryIncomplete"]!.GetValue()); + } + + [Fact] + public async Task InstanceList_PublishedDoubles_ReturnsBothVerifiedInstancesWithoutTokens() + { + await using McpClient client = await fixture.Gateway.ConnectAsync(); + + CallToolResult result = await client.CallToolAsync(GatewayToolCatalog.InstanceListToolName, + new Dictionary(), cancellationToken: Token); + + Assert.NotEqual(true, result.IsError); + JsonObject payload = StructuredPayload(result); + JsonArray instances = payload["instances"]!.AsArray(); + Assert.Equal([fixture.First.Descriptor.InstanceId, fixture.Second.Descriptor.InstanceId], + instances.Select(static instance => instance!["instanceId"]!.GetValue())); + string text = payload.ToJsonString(); + Assert.DoesNotContain(fixture.First.Descriptor.AccessToken, text, StringComparison.Ordinal); + Assert.DoesNotContain(fixture.Second.Descriptor.AccessToken, text, StringComparison.Ordinal); + } + + private static string[] BackendToolNames() + { + return McpPrimitiveCatalog.Create(TestComposition.BackendManifest).Tools.Select(static tool => tool.Name).ToArray(); + } + + private static Dictionary RoutedArguments(FakeBackend backend, string tool, string marker) + { + return new Dictionary(StringComparer.Ordinal) + { + [GatewayToolCatalog.InstanceIdArgumentName] = backend.Descriptor.InstanceId, + ["marker"] = $"{tool}:{marker}", + ["nested"] = new Dictionary { ["route"] = marker, ["tool"] = tool } + }; + } + + private static void AssertError(CallToolResult result, string tool, ToolErrorKind kind) + { + Assert.True(result.IsError); + Assert.Null(result.StructuredContent); + Assert.True(ToolErrorResults.TryRead(result, out ToolError? error)); + Assert.Equal(kind, error.Kind); + Assert.Equal(ToolHostEffect.NotStarted, error.HostEffect); + Assert.Equal(tool, error.Operation); + } + + private static void AssertForwarded(string tool, FakeBackend backend, string marker, CallToolResult result) + { + Assert.NotEqual(true, result.IsError); + Assert.Equal(tool, backend.LastToolName); + Assert.DoesNotContain(GatewayToolCatalog.InstanceIdArgumentName, backend.LastArguments.Keys); + Assert.Equal($"{tool}:{marker}", backend.LastArguments["marker"].GetString()); + Assert.Equal(marker, backend.LastArguments["nested"].GetProperty("route").GetString()); + Assert.Equal(tool, backend.LastArguments["nested"].GetProperty("tool").GetString()); + + JsonObject payload = StructuredPayload(result); + Assert.Equal(backend.Descriptor.InstanceId, payload["backend"]!.GetValue()); + Assert.Equal($"{tool}:{marker}", payload["arguments"]!["marker"]!.GetValue()); + Assert.Equal(marker, payload["arguments"]!["nested"]!["route"]!.GetValue()); + Assert.Equal(tool, payload["arguments"]!["nested"]!["tool"]!.GetValue()); + Assert.Equal(["marker", "nested"], payload["arguments"]!.AsObject().Select(static argument => argument.Key) + .Order(StringComparer.Ordinal)); + } + + private static JsonObject StructuredPayload(CallToolResult result) + { + Assert.True(result.StructuredContent is JsonElement); + JsonElement structured = result.StructuredContent!.Value; + Assert.Equal(structured.GetRawText(), Assert.IsType(Assert.Single(result.Content)).Text); + return JsonNode.Parse(structured.GetRawText())!.AsObject(); + } + + private static void AssertUnchanged(FakeBackend backend, BackendCounts before) + { + Assert.Equal(before.ForwardedCalls, backend.ForwardedCallCount); + Assert.Equal(before.InitializeCalls, backend.InitializeCount); + Assert.Equal(before.IdentityProbes, backend.IdentityProbeCount); + } + + private sealed record BackendCounts(int ForwardedCalls, int InitializeCalls, int IdentityProbes) + { + internal static BackendCounts Capture(FakeBackend backend) + { + return new BackendCounts(backend.ForwardedCallCount, backend.InitializeCount, backend.IdentityProbeCount); + } + } +} + +public sealed class AllToolGatewayFixture : IAsyncLifetime +{ + internal GatewayTestHost Gateway + { + get; + private set; + } = null!; + + internal FakeBackend First + { + get; + private set; + } = null!; + + internal FakeBackend Second + { + get; + private set; + } = null!; + + public async ValueTask InitializeAsync() + { + Gateway = await GatewayTestHost.StartAsync(); + First = await FakeBackend.StartAsync(Gateway.Registry, "first"); + Second = await FakeBackend.StartAsync(Gateway.Registry, "second"); + } + + public async ValueTask DisposeAsync() + { + if (Second is not null) + { + await Second.DisposeAsync(); + } + + if (First is not null) + { + await First.DisposeAsync(); + } + + if (Gateway is not null) + { + await Gateway.DisposeAsync(); + } + } +} diff --git a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs new file mode 100644 index 0000000..9e390df --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveAdditionalReadQualification.cs @@ -0,0 +1,137 @@ +using System.Globalization; +using System.Runtime.Versioning; +using System.Text.Json.Nodes; + +using CheatEngine.Mcp.Core.Contract; + +namespace CheatEngine.Mcp.Tests.LiveQualification; + +/// Known-value orientation checks on the owned fixture before any workflow changes its buffers. +[SupportedOSPlatform("windows")] +internal static class LiveAdditionalReadQualification +{ + internal static async Task VerifyAsync(LiveSandboxSession sandbox, LiveMcpClient gateway) + { + LiveMcpInstanceClient instance = gateway.Bind(sandbox.HostA.InstanceId); + JsonNode current = await CallAsync(instance, CheatEngineToolNames.ProcessGetCurrent); + Assert.True(current["isOpen"]!.GetValue()); + Assert.Equal(sandbox.HostA.TargetProcessId, current["processId"]!.GetValue()); + int pointerSize = current["pointerSize"]!.GetValue(); + Assert.Equal(sandbox.TargetArchitecture == "x64" ? 8 : 4, pointerSize); + // The target is selected before plugin activation, so the initial Client selection epoch may be zero. + long selectionEpoch = current["selectionEpoch"]!.GetValue(); + Assert.True(selectionEpoch >= 0); + sandbox.Record("all_tools_process_get_current", current.DeepClone()); + + JsonNode processes = await CallAsync(instance, CheatEngineToolNames.ProcessList, + new Dictionary + { + ["maximumResults"] = 4096, + ["nameContains"] = sandbox.TargetArchitecture == "x64" ? "CheatEngine.Mcp.LiveTarget" : "dotnet" + }); + Assert.Contains(processes["processes"]!.AsArray(), process => + process!["processId"]!.GetValue() == sandbox.HostA.TargetProcessId); + Assert.False(processes["truncated"]!.GetValue()); + sandbox.Record("all_tools_process_list", new + { + targetFound = true + }); + + JsonNode threads = await CallAsync(instance, CheatEngineToolNames.ProcessListThreads); + Assert.NotEmpty(threads["threads"]!.AsArray()); + Assert.All(threads["threads"]!.AsArray(), thread => Assert.True(thread!.GetValue() > 0)); + Assert.False(threads["truncated"]!.GetValue()); + sandbox.Record("all_tools_process_list_threads", threads.DeepClone()); + + // Set the captured value so failure cannot leave a different target interpretation behind. + JsonNode pointer = await CallAsync(instance, CheatEngineToolNames.ProcessSetPointerSize, + new Dictionary { ["pointerSize"] = pointerSize }); + Assert.Equal(pointerSize, pointer["pointerSize"]!.GetValue()); + JsonNode after = await CallAsync(instance, CheatEngineToolNames.ProcessGetCurrent); + Assert.Equal(pointerSize, after["pointerSize"]!.GetValue()); + Assert.Equal(sandbox.HostA.TargetProcessId, after["processId"]!.GetValue()); + Assert.Equal(selectionEpoch, after["selectionEpoch"]!.GetValue()); + sandbox.Record("all_tools_process_set_pointer_size", new + { + pointerSize + }); + + string address = Parse(sandbox.HostA.TargetAddress).ToString("X", CultureInfo.InvariantCulture); + JsonNode info = await CallAsync(instance, CheatEngineToolNames.MemoryGetAddressInfo, + new Dictionary + { + ["addresses"] = new[] { sandbox.HostA.TargetAddress }, + ["includeRtti"] = false, + ["includePointerValue"] = true + }); + JsonNode item = Assert.Single(info["items"]!.AsArray())!; + Assert.Null(item["error"]); + Assert.Equal(address, item["address"]!.GetValue()); + JsonNode region = item["region"]!; + Assert.Equal("committed", region["state"]!.GetValue()); + Assert.Equal("private", region["type"]!.GetValue()); + Assert.True(region["size"]!.GetValue() > 0); + Assert.True(region["protection"]!["read"]!.GetValue()); + Assert.True(region["protection"]!["write"]!.GetValue()); + Assert.NotNull(item["pointerValue"]); + sandbox.Record("all_tools_memory_get_address_info", info.DeepClone()); + + JsonNode samples = await CallAsync(instance, CheatEngineToolNames.MemoryReadSamples, + new Dictionary + { + ["addresses"] = new[] { sandbox.HostA.TargetAddress }, + ["valueType"] = "int32", + ["intervalMs"] = 25, + ["durationMs"] = 100 + }); + int ticks = samples["ticks"]!.GetValue(); + Assert.True(ticks > 0); + Assert.True(samples["elapsedMs"]!.GetValue() >= 0); + Assert.Null(samples["missedTicks"]); + JsonNode series = Assert.Single(samples["series"]!.AsArray())!; + Assert.Equal(address, series["address"]!.GetValue()); + Assert.Equal(ticks, series["samples"]!.GetValue()); + Assert.Equal(1, series["distinctCount"]!.GetValue()); + Assert.Equal("20260926", series["first"]!.GetValue()); + Assert.Equal("20260926", series["last"]!.GetValue()); + Assert.False(series["truncated"]!.GetValue()); + Assert.Null(series["error"]); + Assert.Single(series["changes"]!.AsArray()); + Assert.Equal("20260926", series["changes"]![0]!["value"]!.GetValue()); + sandbox.Record("all_tools_memory_read_samples", samples.DeepClone()); + + JsonNode api = await CallAsync(instance, CheatEngineToolNames.LuaFindApi, + new Dictionary { ["query"] = "getAddressSafe", ["limit"] = 100, ["offset"] = 0 }); + Assert.True(api["total"]!.GetValue() > 0); + Assert.False(api["truncated"]!.GetValue()); + Assert.NotEmpty(api["lines"]!.AsArray()); + Assert.All(api["lines"]!.AsArray(), line => + { + Assert.True(line!["number"]!.GetValue() > 0); + Assert.Contains("getAddressSafe", line["text"]!.GetValue(), StringComparison.OrdinalIgnoreCase); + }); + sandbox.Record("all_tools_lua_find_api", api.DeepClone()); + + JsonNode decoded = await CallAsync(instance, CheatEngineToolNames.CodeDisassembleBytes, + new Dictionary { ["hexadecimalBytes"] = "90" }); + Assert.Equal("0", decoded["origin"]!.GetValue()); + Assert.Contains("nop", decoded["text"]!.GetValue(), StringComparison.OrdinalIgnoreCase); + JsonNode returned = await CallAsync(instance, CheatEngineToolNames.CodeDisassembleBytes, + new Dictionary { ["hexadecimalBytes"] = "C3" }); + Assert.Equal("0", returned["origin"]!.GetValue()); + Assert.Contains("ret", returned["text"]!.GetValue(), StringComparison.OrdinalIgnoreCase); + sandbox.Record("all_tools_code_disassemble_bytes", new + { + nop = decoded.DeepClone(), + ret = returned.DeepClone() + }); + } + + private static async Task CallAsync(LiveMcpInstanceClient instance, string tool, + IReadOnlyDictionary? arguments = null) => await instance.CallToolAsync(tool, arguments) + ?? throw new InvalidDataException($"Tool '{tool}' returned no payload."); + + private static ulong Parse(string address) => ulong.Parse( + address.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? address[2..] : address, + NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); +} diff --git a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveLifecycleQualification.cs b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveLifecycleQualification.cs index 8200df7..1917592 100644 --- a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveLifecycleQualification.cs +++ b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveLifecycleQualification.cs @@ -8,7 +8,8 @@ internal static class LiveLifecycleQualification [SupportedOSPlatform("windows")] internal static async Task RunAsync(LiveQualificationInputs inputs) { - Assert.Equal(LiveQualificationScenario.Lifecycle, inputs.Scenario); + Assert.True(inputs.Scenario is LiveQualificationScenario.Lifecycle or LiveQualificationScenario.Workflows); + bool reloadPlugins = inputs.Scenario == LiveQualificationScenario.Lifecycle; LiveSandboxSession? sandbox = null; LiveLifecycleSession? connection = null; Exception? failure = null; @@ -17,14 +18,16 @@ internal static async Task RunAsync(LiveQualificationInputs inputs) { sandbox = await LiveSandboxSession.StartAsync(inputs, LiveSandboxOptions.Smoke with { - LifecycleQualification = true + LifecycleQualification = reloadPlugins, + WorkflowQualification = !reloadPlugins }); connection = await LiveLifecycleSession.ConnectAsync(sandbox); await connection.AttachBothAsync(); await connection.VerifyBothAsync(); await LiveResourceQualification.VerifyAsync(sandbox, connection.Gateway); + await LiveAdditionalReadQualification.VerifyAsync(sandbox, connection.Gateway); await LiveWorkflowQualification.VerifyAsync(sandbox, connection.Gateway); - for (int cycle = 0; cycle < 3; cycle++) + for (int cycle = 0; cycle < (reloadPlugins ? 3 : 0); cycle++) { await connection.CyclePluginAsync("A"); } @@ -42,9 +45,9 @@ internal static async Task RunAsync(LiveQualificationInputs inputs) await connection.RejectStaleAsync(before.InstanceId); await connection.AttachBothAsync(); await connection.VerifyBothAsync(); - sandbox.Record("lifecycle_workload", new + sandbox.Record(reloadPlugins ? "lifecycle_workload" : "workflow_restart_workload", new { - pluginCycles = 3, + pluginCycles = reloadPlugins ? 3 : 0, targetRestarts = 1, gatewayRestarts = 1, hostRestarts = 1 diff --git a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationOptIn.cs b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationOptIn.cs index 01a7784..a43b5eb 100644 --- a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationOptIn.cs +++ b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationOptIn.cs @@ -55,6 +55,7 @@ internal static LiveQualificationDecision Evaluate(Func variabl "compiler-extended" => LiveQualificationScenario.CompilerExtended, "compiler-injection" => LiveQualificationScenario.CompilerInjection, "lifecycle" => LiveQualificationScenario.Lifecycle, + "workflows" => LiveQualificationScenario.Workflows, "performance" => LiveQualificationScenario.Performance, "soak" => LiveQualificationScenario.Soak, "dispatch-diagnostic" => LiveQualificationScenario.DispatchDiagnostic, @@ -62,7 +63,7 @@ internal static LiveQualificationDecision Evaluate(Func variabl }; if (selectedScenario is null) { - return Refuse($"{ScenarioVariable} must be smoke, compiler, compiler-extended, compiler-injection, lifecycle, performance, soak, or dispatch-diagnostic."); + return Refuse($"{ScenarioVariable} must be smoke, compiler, compiler-extended, compiler-injection, lifecycle, workflows, performance, soak, or dispatch-diagnostic."); } LiveQualificationScenario scenario = selectedScenario.Value; diff --git a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationOptInTests.cs b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationOptInTests.cs index 66bb447..ef9096c 100644 --- a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationOptInTests.cs +++ b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationOptInTests.cs @@ -170,6 +170,7 @@ public void Evaluate_MissingOrRelativeInstallation_Refuses(string installation) [Theory] [InlineData("lifecycle", LiveQualificationScenario.Lifecycle)] + [InlineData("workflows", LiveQualificationScenario.Workflows)] [InlineData("performance", LiveQualificationScenario.Performance)] [InlineData("soak", LiveQualificationScenario.Soak)] [InlineData("dispatch-diagnostic", LiveQualificationScenario.DispatchDiagnostic)] diff --git a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationScenario.cs b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationScenario.cs index 59efe92..0845fe8 100644 --- a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationScenario.cs +++ b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveQualificationScenario.cs @@ -10,5 +10,6 @@ internal enum LiveQualificationScenario Lifecycle, Performance, Soak, - DispatchDiagnostic + DispatchDiagnostic, + Workflows } diff --git a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveSandboxOptions.cs b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveSandboxOptions.cs index dffce7a..6de4fbb 100644 --- a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveSandboxOptions.cs +++ b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveSandboxOptions.cs @@ -6,7 +6,7 @@ internal sealed record LiveSandboxOptions(bool CompilerQualification, bool Enabl internal static LiveSandboxOptions Smoke { get; } = new(false, false); internal bool RequiresCapabilityConfiguration => CompilerQualification || LifecycleQualification - || PerformanceQualification || SoakQualification || DispatchDiagnostic; + || PerformanceQualification || SoakQualification || DispatchDiagnostic || WorkflowQualification; internal LiveCompilerVariant Variant { get; init; } = LiveCompilerVariant.Normal; internal CompilerTempTopology TempTopology { get; init; } = CompilerTempTopology.Isolated; @@ -18,6 +18,10 @@ internal bool LifecycleQualification { get; init; } + internal bool WorkflowQualification + { + get; init; + } internal bool PerformanceQualification { get; init; @@ -51,6 +55,7 @@ internal void ValidateFor(LiveQualificationScenario scenario) && (!EnableTargetCodeExecution || compiler) && (!injection || EnableTargetCodeExecution) && LifecycleQualification == (scenario == LiveQualificationScenario.Lifecycle) + && WorkflowQualification == (scenario == LiveQualificationScenario.Workflows) && PerformanceQualification == (scenario == LiveQualificationScenario.Performance) && SoakQualification == (scenario == LiveQualificationScenario.Soak) && DispatchDiagnostic == (scenario == LiveQualificationScenario.DispatchDiagnostic) diff --git a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveSandboxOptionsTests.cs b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveSandboxOptionsTests.cs index a1c8d67..855371d 100644 --- a/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveSandboxOptionsTests.cs +++ b/tests/CheatEngine.Mcp.Tests/LiveQualification/LiveSandboxOptionsTests.cs @@ -22,6 +22,10 @@ public void ValidateFor_ExactScenarioOptions_Accept() LifecycleQualification = true }).ValidateFor(LiveQualificationScenario.Lifecycle); (LiveSandboxOptions.Smoke with + { + WorkflowQualification = true + }).ValidateFor(LiveQualificationScenario.Workflows); + (LiveSandboxOptions.Smoke with { PerformanceQualification = true }).ValidateFor(LiveQualificationScenario.Performance); @@ -46,6 +50,7 @@ public void ValidateFor_SmokeRejectsEveryExtraCapabilityAndLifetime() LiveSandboxOptions.Smoke with { TempTopology = CompilerTempTopology.Shared }, LiveSandboxOptions.Smoke with { EnableManagedInjection = true }, LiveSandboxOptions.Smoke with { LifecycleQualification = true }, + LiveSandboxOptions.Smoke with { WorkflowQualification = true }, LiveSandboxOptions.Smoke with { PerformanceQualification = true }, LiveSandboxOptions.Smoke with { SoakQualification = true }, LiveSandboxOptions.Smoke with { DispatchDiagnostic = true }, @@ -57,6 +62,20 @@ public void ValidateFor_SmokeRejectsEveryExtraCapabilityAndLifetime() } } + [Fact] + public void ValidateFor_WorkflowsRequireDisabledExecutionAndDedicatedConfiguration() + { + LiveSandboxOptions workflows = LiveSandboxOptions.Smoke with + { + WorkflowQualification = true + }; + Assert.True(workflows.RequiresCapabilityConfiguration); + Assert.False(workflows.EnableTargetCodeExecution); + workflows.ValidateFor(LiveQualificationScenario.Workflows); + Assert.Throws(() => (workflows with { EnableTargetCodeExecution = true }).ValidateFor(LiveQualificationScenario.Workflows)); + Assert.Throws(() => (workflows with { LifecycleQualification = true }).ValidateFor(LiveQualificationScenario.Workflows)); + } + [Fact] public void ValidateFor_DispatchDiagnosticRejectsExecutionAndUnrelatedScenarios() { diff --git a/tests/CheatEngine.Mcp.Tests/LiveQualification/McpLiveQualificationTests.cs b/tests/CheatEngine.Mcp.Tests/LiveQualification/McpLiveQualificationTests.cs index 6372716..ebaf26e 100644 --- a/tests/CheatEngine.Mcp.Tests/LiveQualification/McpLiveQualificationTests.cs +++ b/tests/CheatEngine.Mcp.Tests/LiveQualification/McpLiveQualificationTests.cs @@ -31,7 +31,7 @@ public async Task OneGateway_TwoOwnedInstances_RoutesStateAndSurvivesOneHostShut await LivePerformanceQualification.RunAsync(inputs); return; } - if (inputs.Scenario == LiveQualificationScenario.Lifecycle) + if (inputs.Scenario is LiveQualificationScenario.Lifecycle or LiveQualificationScenario.Workflows) { await LiveLifecycleQualification.RunAsync(inputs); return; diff --git a/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaCodeQualificationTests.cs b/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaCodeQualificationTests.cs new file mode 100644 index 0000000..70a4db7 --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaCodeQualificationTests.cs @@ -0,0 +1,52 @@ +using CheatEngine.Mcp.Core.Execution; +using CheatEngine.Mcp.Core.Features; +using CheatEngine.Mcp.Core.Jobs; +using CheatEngine.Mcp.Tools.Code; + +using Microsoft.Extensions.Options; + +namespace CheatEngine.Mcp.Tests.NativeLua; + +/// Executable CE-API-double coverage for the fixed code-view Lua bodies. +public sealed partial class NativeLuaToolRuntimeTests +{ + [Fact] + public void CodeGetFunction_StubbedApi_ReturnsBoundariesAndJumpDestination() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(""" + getAddressSafe = function(value) receivedAddress = value; return 0x401020 end + getFunctionRange = function(address) assert(address == 0x401020); return 0x401000, 0x401080 end + isJumpDestination = function(address, range) receivedRange = range; return address == 0x401020 end + """); + ToolDispatch dispatch = CreateNativeDispatch(new McpFeatureOptions()); + + CodeLuaFunction result = dispatch.RunLua("code_get_function", CodeScripts.GetFunction, + CodeLuaJsonContext.Default.CodeLuaFunction, CancellationToken.None, "game.exe+20", 512); + + Assert.Equal(new CodeLuaFunction(true, "401020", "401000", "401080", 128, true), result); + Assert.Equal(("game.exe+20", 512L), (ReadGlobal("receivedAddress"), ReadGlobal("receivedRange"))); + } + + [Fact] + public void CodeComments_StubbedApi_KeepsReadOrderAndWritesTheRequestedComment() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(""" + getAddressSafe = function(value) return ({ first=0x401000, second=0x402000, target=0x403000 })[value] end + getComment = function(address) return address == 0x401000 and 'entry' or nil end + setComment = function(address, comment) writtenAddress=address; writtenComment=comment end + """); + ToolDispatch dispatch = CreateNativeDispatch(new McpFeatureOptions()); + + using JobRegistry jobs = new(dispatch, new TargetResources(), Options.Create(new McpExecutionOptions()), + TimeProvider.System); + CodeTools tools = new(dispatch, jobs); + CodeComments read = tools.GetComments(["first", "second"], CancellationToken.None); + CodeComment written = tools.SetComment("target", "updated", CancellationToken.None); + + Assert.Equal([new CodeComment("401000", "entry"), new CodeComment("402000", null)], read.Comments); + Assert.Equal(new CodeComment("403000", "updated"), written); + Assert.Equal((0x403000L, "updated"), (ReadGlobal("writtenAddress"), ReadGlobal("writtenComment"))); + } +} diff --git a/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs b/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs new file mode 100644 index 0000000..5ef6aab --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaDebuggerAdapterQualificationTests.cs @@ -0,0 +1,208 @@ +using System.Reflection; + +using CheatEngine.Client; +using CheatEngine.Client.Inspection; +using CheatEngine.Client.Lua; +using CheatEngine.Client.Processes; +using CheatEngine.Client.Results; +using CheatEngine.Client.Runtime; +using CheatEngine.Mcp.Core.Features; +using CheatEngine.Mcp.Core.Jobs; +using CheatEngine.Mcp.Tests.Support; +using CheatEngine.Mcp.Tools.Debugger; +using CheatEngine.SDK.Engine.Inspection; +using CheatEngine.SDK.Engine.Runtime; +using CheatEngine.SDK.Engine.Values; +using CheatEngine.SDK.Lua.Calls; + +using Microsoft.Extensions.Options; + +namespace CheatEngine.Mcp.Tests.NativeLua; + +/// +/// Exercises the public debugger adapters through the native fixed-Lua bridge. The Lua functions are inert CE API +/// doubles: they only capture arguments and return deterministic debugger state. +/// +public sealed partial class NativeLuaToolRuntimeTests +{ + [Fact] + public void DebuggerAdapter_AttachAndControlOperations_ForwardKnownArgumentsAndResults() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(""" + attached = false + RAX, RIP, RSP = 1, 0x401000, 0x7000 + co_run, co_stepinto, co_stepover = 10, 11, 12 + bptExecute, bptAccess, bptWrite = 1, 2, 3 + debug_isDebugging = function() return attached end + debug_canBreak = function() return true end + debug_isBroken = function() return true end + debug_isStepping = function() return false end + debug_getCurrentDebuggerInterface = function() return 1 end + debug_getContext = function(_) return attached end + debug_getCurrentContextTable = function(_) return {RAX = RAX, RIP = RIP, RSP = RSP} end + targetIs64Bit = function() return true end + targetIsX86 = function() return true end + getOpenedProcessID = function() return 77 end + getProcesslist = function() return {[77] = true} end + debugProcess = function(value) attached = true; requestedInterface = value end + debug_breakThread = function(value) breakThread = value end + continueModes = {} + debug_continueFromBreakpoint = function(value) continueModes[#continueModes + 1] = value end + debug_setContext = function(_) return true end + debug_updateGUI = function() guiUpdates = (guiUpdates or 0) + 1 end + debug_addThreadToNoBreakList = function(value) ignoredThread, ignored = value, true end + debug_removeThreadFromNoBreakList = function(value) ignoredThread, ignored = value, false end + readQword = function(address) return address == 0x7000 and 0x401005 or nil end + getPreviousOpcode = function(address) return address - 5 end + disassemble = function(address) return address == 0x401000 and 'call target' or 'nop' end + """); + using DebuggerToolHarness harness = CreateDebuggerTools(withProcess: true); + DebuggerTools tools = harness.Tools; + + DebuggerAttachment attached = tools.Attach(DebuggerInterface.Windows, Token); + DebuggerStatus status = tools.GetStatus(Token); + DebuggerBreakRequested broken = tools.BreakThread(19, Token); + DebuggerExecutionContinued continued = tools.Continue(Token); + DebuggerExecutionContinued stepped = tools.Step(DebuggerStepMode.Over, Token); + DebuggerContext context = tools.GetContext(false, Token); + DebuggerRegisterSet written = tools.SetRegister("eax", -1, Token); + DebuggerThreadIgnoreChanged ignored = tools.SetThreadIgnored(19, true, Token); + DebuggerStackTrace trace = tools.GetStackTrace(1, Token); + + Assert.Equal((true, false, 77, DebuggerInterface.Windows), + (attached.Attached, attached.AlreadyAttached, attached.ProcessId, attached.ActiveInterface)); + Assert.Equal(1L, ReadGlobal("requestedInterface")); + Assert.Equal((true, true, DebuggerInterface.Windows), (status.StateValid, status.Broken, status.ActiveInterface)); + Assert.Equal(new DebuggerBreakRequested(19, true), broken); + Assert.Equal(("run", true), (continued.Mode, continued.Continued)); + Assert.Equal(("over", true), (stepped.Mode, stepped.Continued)); + object?[] continueModes = (object?[]) ReadGlobal("continueModes")!; + Assert.Equal([10L, 12L], continueModes); + Assert.Equal("1", context.Registers["RAX"]); + Assert.Equal(new DebuggerRegisterSet("EAX", "RAX", "FFFFFFFF", true), written); + Assert.Equal((4294967295L, 1L), (ReadGlobal("RAX"), ReadGlobal("guiUpdates"))); + Assert.Equal(new DebuggerThreadIgnoreChanged(19, true), ignored); + Assert.Equal((19L, true), (ReadGlobal("ignoredThread"), ReadGlobal("ignored"))); + DebuggerStackFrame frame = Assert.Single(trace.Frames); + Assert.Equal(("7000", "401005", "call target", true), + (frame.StackAddress, frame.ReturnAddress, frame.CallInstruction, frame.IsHeuristic)); + } + + [Fact] + public void DebuggerAdapter_BreakpointLifecycle_TracksThenReleasesTheKnownOwnedBreakpoint() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(JobHostStubs + "\n" + """ + bptExecute, bptAccess, bptWrite, bpmDebugRegister, bpmInt3, bpmException = 1, 2, 3, 4, 5, 6 + debug_isDebugging = function() return true end + getAddressSafe = function(value) observedExpression = value; return 0x401000 end + debug_setBreakpoint = function(address, size, trigger, method, callback) + observedBreakpoint = {address, size, trigger, method, callback}; return true, 88 + end + debug_removeBreakpointByID = function(id) removedBreakpoint = id; return true end + targetIs64Bit = function() return true end + """); + using DebuggerToolHarness harness = CreateDebuggerTools(); + DebuggerTools tools = harness.Tools; + + DebuggerBreakpointSet set = tools.SetBreakpoint("game.exe+1000", DebuggerBreakpointTrigger.Execute, 1, + DebuggerBreakpointMethod.Hardware, null, false, Token); + DebuggerBreakpointDeleted deleted = tools.DeleteBreakpoint("game.exe+1000", Token); + + Assert.Equal(("401000", DebuggerBreakpointTrigger.Execute, 1, DebuggerBreakpointMethod.Hardware, false), + (set.Address, set.Trigger, set.Size, set.Method, set.OneShot)); + Assert.Equal("game.exe+1000", ReadGlobal("observedExpression")); + object?[] breakpointArguments = (object?[]) ReadGlobal("observedBreakpoint")!; + Assert.Equal([0x401000L, 1L, 1L, 4L], breakpointArguments.Take(4)); + Assert.True(deleted.Released); + Assert.Equal(set.ResourceId, deleted.ResourceId); + Assert.Equal(88L, ReadGlobal("removedBreakpoint")); + } + + [Fact] + public void DebuggerAdapter_CaptureTraceAndRunTo_CreateBoundedJobsWithKnownReceipts() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(JobHostStubs + "\n" + CaptureHostStubs + "\n" + TraceHostStubs + "\n" + """ + co_run = 3 + debug_continueFromBreakpoint = function(value) lastContinue = value end + """); + using DebuggerToolHarness harness = CreateDebuggerTools(); + DebuggerTools tools = harness.Tools; + + DebuggerJobStarted capture = tools.StartCapture("401000", DebuggerBreakpointTrigger.Execute, 1, + maximumHits: 4, lifetimeSeconds: 10, cancellationToken: Token); + InstallStubs("captureCallback()"); + DebuggerJobStarted trace = tools.StartTrace("401000", DebuggerStepMode.Into, 3, "RAX=1", false, 10, Token); + InstallStubs("RAX = 1; RIP = 0x401000; captureCallback()"); + InstallStubs("debug_getContext = function(_) return true end"); + DebuggerRunToStarted runTo = tools.RunTo("401000", 10, Token); + DebuggerCapturePage capturePage = tools.PollCapture(capture.JobId, 0, 4, Token); + DebuggerTracePage tracePage = tools.PollTrace(trace.JobId, 0, 3, Token); + + Assert.All(new[] { capture.JobId, trace.JobId, runTo.JobId }, static id => Assert.StartsWith("debug", id)); + Assert.Equal((77, "401000", 10), (capture.ProcessId, capture.Address, capture.LifetimeSeconds)); + Assert.Equal((77, "401000", 10), (trace.ProcessId, trace.Address, trace.LifetimeSeconds)); + Assert.Equal((77, "401000", 10), (runTo.ProcessId, runTo.Address, runTo.LifetimeSeconds)); + Assert.Equal("401000", Assert.Single(capturePage.Hits).Context.Ip); + Assert.Equal("1", Assert.Single(tracePage.Steps).Registers["RAX"]); + Assert.Equal(3L, ReadGlobal("lastContinue")); + } + + private static DebuggerToolHarness CreateDebuggerTools(bool withProcess = false) + { + IOptions options = Options.Create(new McpExecutionOptions()); + ICheatEngineClient client = withProcess ? CreateDebuggerClientWithProcess() : CreateJsonLuaClient(); + ToolDispatch dispatch = new(client, new McpFeatureGate(Options.Create(new McpFeatureOptions())), options, + new DispatchStatistics(options), TimeProvider.System, new RecordingLogger(), + new PluginFixedLuaExecutor(client)); + TargetResources resources = new(new McpStateLedger(dispatch, TimeProvider.System)); + JobRegistry jobs = new(dispatch, resources, options, TimeProvider.System); + return new DebuggerToolHarness(new DebuggerTools(dispatch, jobs, resources), jobs, resources); + } + + private sealed class DebuggerToolHarness(DebuggerTools tools, JobRegistry jobs, TargetResources resources) : IDisposable + { + public DebuggerTools Tools => tools; + + public void Dispose() + { + ReleaseAllResult released = resources.ReleaseAll(CancellationToken.None, includeOrphans: true); + if (!released.IsComplete) + { + throw new InvalidOperationException("Debugger adapter test cleanup did not complete."); + } + + jobs.Dispose(); + } + } + + private static ICheatEngineClient CreateDebuggerClientWithProcess() + { + ILuaClient lua = ClientTestDouble.Create((method, arguments) => + { + Assert.Equal(nameof(ILuaClient.Execute), method.Name); + Type resultType = method.GetGenericArguments()[1]; + MethodInfo tryExecute = typeof(ILuaOperation<>).MakeGenericType(resultType) + .GetMethod(nameof(ILuaOperation<>.TryExecute))!; + object?[] call = [ActiveContext.Instance, null, null]; + if ((bool) tryExecute.Invoke(arguments![0], call)!) + { + return call[1]; + } + + ((CheatEngineFailure) call[2]!).Throw(); + return null; + }); + IProcessClient processes = ClientTestDouble.Create((method, _) => method.Name switch + { + nameof(IProcessClient.GetCurrentProcess) => new ProcessSnapshot(new TargetProcessId(77), "owned.exe", null, + TargetBackend.LocalProcess, CheatEngineArchitecture.X64, PointerSize.Bit64, 8, + new DateTimeOffset(2026, 10, 11, 0, 0, 0, TimeSpan.Zero), 1), + _ => throw new NotSupportedException(method.Name) + }); + return ClientTestDouble.Client((nameof(ICheatEngineClient.Lua), lua), + (nameof(ICheatEngineClient.Processes), processes)); + } +} diff --git a/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs b/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs new file mode 100644 index 0000000..0c32949 --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaExecutionAdapterQualificationTests.cs @@ -0,0 +1,284 @@ +using CheatEngine.Client; +using CheatEngine.Client.Inspection; + +using CheatEngine.Mcp.Core.Features; +using CheatEngine.Mcp.Core.Files; +using CheatEngine.Mcp.Core.Jobs; +using CheatEngine.Mcp.Core.Targets; +using CheatEngine.Mcp.Tests.Core; +using CheatEngine.Mcp.Tests.Support; +using CheatEngine.Mcp.Tools.Exec; +using CheatEngine.Mcp.Tools.Kernel; + +using CheatEngine.SDK.Engine.Inspection; +using CheatEngine.SDK.Engine.Values; + +using Microsoft.Extensions.Options; + +namespace CheatEngine.Mcp.Tests.NativeLua; + +/// +/// Exercises the public execution and kernel adapters through the real Native Lua bridge. The Lua functions below +/// are inert doubles: they record arguments and return deterministic values without compiling, injecting, calling +/// a target, or accessing DBK/DBVM. +/// +public sealed partial class NativeLuaToolRuntimeTests +{ + [Fact] + public void ExecutionAdapter_CallLocal_EnablesTheFeatureAndForwardsTheKnownResult() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(CallLocalStubs); + ExecTools tools = new(CreateNativeDispatch(new McpFeatureOptions { EnableTargetCodeExecution = true }), + CreateFiles()); + + ExecCallResult result = tools.CallLocal("localcall", -1, Token); + + Assert.Equal(new ExecCallResult("1000", "-1"), result); + Assert.Equal("FFFFFFFFFFFFFFFF", ReadGlobal("localParameter")); + } + + [Fact] + public void ExecutionAdapter_CompileC_TargetSelfForwardsFlagsAndSortsTheKnownSymbols() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(""" + compile = function(source, address, targetSelf, kernelMode, noDebug) + observedSource, observedAddress, observedTargetSelf, observedKernelMode, observedNoDebug = + source, address, targetSelf, kernelMode, noDebug + return {zeta = 0x2000, alpha = 0x1000, ignored = 'not an address'} + end + """); + ExecTools tools = new(CreateNativeDispatch(new McpFeatureOptions { EnableTargetCodeExecution = true }), + CreateFiles()); + + ExecCompileResult result = tools.CompileC("int entry(void) { return 7; }", targetSelf: true, noDebug: false, + cancellationToken: Token); + + Assert.Equal(new[] { new ExecCompiledSymbol("alpha", "1000"), new ExecCompiledSymbol("zeta", "2000") }, + result.Symbols); + Assert.False(result.SymbolsTruncated); + Assert.True(result.TargetSelf); + Assert.False(result.KernelMode); + Assert.Equal("int entry(void) { return 7; }", ReadGlobal("observedSource")); + Assert.Null(ReadGlobal("observedAddress")); + Assert.Equal((true, false, false), (ReadGlobal("observedTargetSelf"), ReadGlobal("observedKernelMode"), + ReadGlobal("observedNoDebug"))); + } + + [Fact] + public void ExecutionAdapter_CompileCSharp_ExportsTheNativeCompilerArtifact() + { + using RuntimeScope scope = CreateScope(); + using McpFilePathsTests.Scratch scratch = new(); + string generated = Path.Combine(scratch.Root, "ce-generated.dll"); + string output = Path.Combine(scratch.Root, "exported.dll"); + byte[] bytes = [0x4D, 0x5A, 0x90, 0x00]; + File.WriteAllBytes(generated, bytes); + InstallStubs($""" + compileCS = function(source, references, coreAssembly) + observedCSharpSource, observedReferenceCount, observedCoreAssembly = source, #references, coreAssembly + return '{generated.Replace("\\", "\\\\", StringComparison.Ordinal)}' + end + """); + McpFilePaths files = new(new McpFileOptions { AllowedRoots = [scratch.Root] }, + Path.Combine(scratch.Root, "registry"), Path.Combine(scratch.Root, "data")); + ExecCSharpTools tools = new(CreateNativeDispatch(new McpFeatureOptions { EnableTargetCodeExecution = true }), + files); + + ExecCSharpCompileResult result = tools.CompileCSharp("public static class Entry {}", output, + cancellationToken: Token); + + Assert.Equal(output, result.OutputPath); + Assert.Equal(bytes.Length, result.Length); + Assert.Equal(bytes, File.ReadAllBytes(output)); + Assert.Equal(("public static class Entry {}", 0L), + (ReadGlobal("observedCSharpSource"), ReadGlobal("observedReferenceCount"))); + Assert.Null(ReadGlobal("observedCoreAssembly")); + } + + [Fact] + public void ExecutionAdapter_CallRemote_ResolvesTheFunctionAndForwardsTypedArguments() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(CallStubs + "\nwideTarget = true"); + ExecTools tools = new(CreateResolvedNativeDispatch(), CreateFiles()); + + ExecCallResult result = tools.CallRemote("function", ExecCallingConvention.Cdecl, 123, + [new ExecCallArgument(ExecArgumentType.Integral, "7")], Token); + + Assert.Equal(new ExecCallResult("1234", "5"), result); + Assert.Equal((1L, "rcx=7"), (ReadGlobal("remoteCalls"), ReadGlobal("lastCall"))); + } + + [Fact] + public void ExecutionAdapter_CallMethod_ResolvesMethodAndInstanceBeforeCalling() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(CallStubs); + ExecTools tools = new(CreateResolvedNativeDispatch(), CreateFiles()); + + ExecCallResult result = tools.CallMethod("function", "instance", 1, cancellationToken: Token); + + Assert.Equal(new ExecCallResult("1234", "5"), result); + Assert.Equal((1L, 1L, 0x5678L), + (ReadGlobal("methodCalls"), ReadGlobal("methodRegister"), ReadGlobal("methodInstance"))); + } + + [Fact] + public void ExecutionAdapter_InjectLibrary_HoldsTheApprovedArtifactAndForwardsTheWaitFlag() + { + using RuntimeScope scope = CreateScope(); + using McpFilePathsTests.Scratch scratch = new(); + string library = Path.Combine(scratch.Root, "fixture.dll"); + File.WriteAllBytes(library, [0x4D, 0x5A]); + InstallStubs(""" + injectLibrary = function(path, skipWait) + observedLibrary, observedSkipWait = path, skipWait + return true + end + """); + ExecTools tools = new(CreateNativeDispatch(new McpFeatureOptions { EnableTargetCodeExecution = true }), + CreateFiles(scratch.Root)); + + ExecLibraryInjection result = tools.InjectLibrary(library, true, Token); + + Assert.Equal(new ExecLibraryInjection(library, true), result); + Assert.Equal((library, true), (ReadGlobal("observedLibrary"), ReadGlobal("observedSkipWait"))); + File.AppendAllText(library, "released"); + } + + [Fact] + public void ExecutionAdapter_InjectDotNet_HoldsTheApprovedAssemblyAndForwardsEveryArgument() + { + using RuntimeScope scope = CreateScope(); + using McpFilePathsTests.Scratch scratch = new(); + string assembly = Path.Combine(scratch.Root, "fixture.dll"); + File.WriteAllBytes(assembly, [0x4D, 0x5A]); + InstallStubs(""" + injectDotNetDLL = function(path, className, methodName, parameter) + observedAssembly, observedClass, observedMethod, observedParameter = path, className, methodName, parameter + return 42 + end + """); + ExecTools tools = new(CreateNativeDispatch(new McpFeatureOptions { EnableTargetCodeExecution = true }), + CreateFiles(scratch.Root)); + + ExecDotNetInjection result = tools.InjectDotNet(assembly, "Fixture.Entry", "Run", "hello", Token); + + Assert.Equal(new ExecDotNetInjection(assembly, "Fixture.Entry", "Run", "42"), result); + Assert.Equal((assembly, "Fixture.Entry", "Run", "hello"), + (ReadGlobal("observedAssembly"), ReadGlobal("observedClass"), ReadGlobal("observedMethod"), + ReadGlobal("observedParameter"))); + File.AppendAllText(assembly, "released"); + } + + [Fact] + public void KernelAdapter_GetStatus_EnablesKernelFeatureAndCopiesKnownState() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(""" + dbk_initialized = function() return true end + dbvm_initialized = function() return false end + dbk_getCR0 = function() return 0x80050033 end + dbk_getCR3 = function() return 0x1AB000 end + dbk_getCR4 = function() return 0x6F0 end + dbvm_getCR4 = function() return 0x690 end + """); + + KernelStatus result = CreateKernelTools().GetStatus(Token); + + Assert.Equal(new KernelStatus(true, false, "80050033", "1AB000", "6F0", "690"), result); + } + + [Fact] + public void KernelAdapter_ReadPhysical_EnablesKernelFeatureAndReturnsTheRequestedByteRange() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(""" + dbvm_initialized = function() return true end + dbvm_readPhysicalMemory = function(address, size) + observedAddress, observedSize = address, size + return {0x48, 0x8B, 0x05} + end + """); + + KernelPhysicalRead result = CreateKernelTools().ReadPhysical("1ab000", 3, Token); + + Assert.Equal(new KernelPhysicalRead("1AB000", "48 8B 05"), result); + Assert.Equal((0x1AB000L, 3L), (ReadGlobal("observedAddress"), ReadGlobal("observedSize"))); + } + + [Fact] + public void KernelAdapter_WritePhysical_EnablesKernelFeatureAndForwardsEachByte() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(""" + dbvm_initialized = function() return true end + dbvm_writePhysicalMemory = function(address, bytes) + observedAddress, observedFirst, observedCount = address, bytes[1], #bytes + return true + end + """); + + KernelPhysicalWrite result = CreateKernelTools().WritePhysical("1AB000", "48 8B 05", Token); + + Assert.Equal(new KernelPhysicalWrite("1AB000", 3), result); + Assert.Equal((0x1AB000L, 0x48L, 3L), (ReadGlobal("observedAddress"), ReadGlobal("observedFirst"), + ReadGlobal("observedCount"))); + } + + [Fact] + public void KernelAdapter_TranslateAddress_ResolvesTheVirtualAddressBeforeTheDbkCall() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(""" + dbk_getPhysicalAddress = function(address) + observedVirtualAddress = address + return 0x1AB000 + end + """); + IOptions options = Options.Create(new McpExecutionOptions()); + ToolDispatch dispatch = CreateResolvedNativeDispatch(new McpFeatureOptions { EnableKernelAccess = true }); + KernelTools tools = new(dispatch, new JobRegistry(dispatch, new TargetResources(), options, TimeProvider.System)); + + KernelAddressTranslation result = tools.TranslateAddress("function", Token); + + Assert.Equal(new KernelAddressTranslation("1234", "1AB000"), result); + Assert.Equal(0x1234L, ReadGlobal("observedVirtualAddress")); + } + + private static McpFilePaths CreateFiles(string? allowedRoot = null) + { + return allowedRoot is null + ? new McpFilePaths(new McpFileOptions(), AppContext.BaseDirectory, AppContext.BaseDirectory) + : new McpFilePaths(new McpFileOptions { AllowedRoots = [allowedRoot] }, Path.Combine(allowedRoot, "registry"), + Path.Combine(allowedRoot, "data")); + } + + private static KernelTools CreateKernelTools() + { + IOptions options = Options.Create(new McpExecutionOptions()); + ToolDispatch dispatch = CreateNativeDispatch(new McpFeatureOptions { EnableKernelAccess = true }); + return new KernelTools(dispatch, new JobRegistry(dispatch, new TargetResources(), options, TimeProvider.System)); + } + + private static ToolDispatch CreateResolvedNativeDispatch(McpFeatureOptions? features = null) + { + IInspectionClient inspection = ClientTestDouble.Create((method, arguments) => + { + Assert.Equal(nameof(IInspectionClient.TryResolveAddress), method.Name); + SymbolExpression expression = Assert.IsType(arguments![0]); + arguments[2] = new Address(expression.Value == "instance" ? 0x5678UL : 0x1234UL); + arguments[3] = default(CheatEngine.Client.Results.CheatEngineFailure); + return true; + }); + ICheatEngineClient client = ClientTestDouble.Client( + (nameof(ICheatEngineClient.Lua), CreateJsonLuaClient().Lua), + (nameof(ICheatEngineClient.Inspection), inspection)); + IOptions options = Options.Create(new McpExecutionOptions()); + return new ToolDispatch(client, new McpFeatureGate(Options.Create(features ?? + new McpFeatureOptions { EnableTargetCodeExecution = true })), options, new DispatchStatistics(options), + TimeProvider.System, new RecordingLogger(), new PluginFixedLuaExecutor(client)); + } +} diff --git a/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs b/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs index 5ca9f07..f634e8f 100644 --- a/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs +++ b/tests/CheatEngine.Mcp.Tests/NativeLua/NativeLuaRuntimeProcessScanV2Tests.cs @@ -192,10 +192,10 @@ public void ProcessV2_FixedEffects_ReturnObservedTypedHostState() threadLists=0; threadListsDestroyed=0 createStringList=function() threadLists=threadLists+1 - return {Count=0, Strings={}, destroy=function() threadListsDestroyed=threadListsDestroyed+1 end} + return {Count=0, destroy=function() threadListsDestroyed=threadListsDestroyed+1 end} end getThreadlist=function(list) - list.Strings[0]='64'; list.Strings[1]='C8'; list.Strings[2]='12C'; list.Count=3 + list[0]='64'; list[1]='C8'; list[2]='12C'; list.Count=3 end setPointerSize=function(value) pointerSize=value end getPointerSize=function() return pointerSize end @@ -238,6 +238,58 @@ public void ProcessV2_FixedEffects_ReturnObservedTypedHostState() } } + [Fact] + public void ProcessV2_ThreadList_UsesDocumentedZeroBasedStringListIndexerForEmptyAndTruncatedResults() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(""" + pid=77; calls=0; lists=0; destroyed=0 + getOpenedProcessID=function() return pid end + createStringList=function() + lists=lists+1 + return {Count=0, destroy=function() destroyed=destroyed+1 end} + end + getThreadlist=function(list) + calls=calls+1 + if calls == 1 then list.Count=0; return end + for i=0,4096 do list[i]=string.format('%X', i+1) end + list.Count=4097 + end + """); + ProcessTools tools = new(CreateNativeProcessDispatch(77), new TargetResources(), + new TargetTransitionGuards([]), CreateProcessFiles()); + + ProcessThreadListResult empty = tools.ListThreads(Token); + ProcessThreadListResult truncated = tools.ListThreads(Token); + + Assert.Empty(empty.Threads); + Assert.False(empty.Truncated); + Assert.Equal((4096, true, 1L, 4096L), + (truncated.Threads.Length, truncated.Truncated, truncated.Threads[0], truncated.Threads[^1])); + Assert.Equal((2L, 2L), (ReadGlobal("lists"), ReadGlobal("destroyed"))); + } + + [Fact] + public void ProcessV2_ThreadList_InvalidHexadecimalEntry_DestroysItsCallerOwnedStringList() + { + using RuntimeScope scope = CreateScope(); + InstallStubs(""" + pid=77; lists=0; destroyed=0 + getOpenedProcessID=function() return pid end + createStringList=function() + lists=lists+1 + return {Count=0, destroy=function() destroyed=destroyed+1 end} + end + getThreadlist=function(list) list[0]='not-hex'; list.Count=1 end + """); + ProcessTools tools = new(CreateNativeProcessDispatch(77), new TargetResources(), + new TargetTransitionGuards([]), CreateProcessFiles()); + + Assert.Throws(() => tools.ListThreads(Token)); + + Assert.Equal((1L, 1L), (ReadGlobal("lists"), ReadGlobal("destroyed"))); + } + [Fact] public void ProcessV2_ThreadList_DestroysItsCallerOwnedStringListWhenCollectionFails() { @@ -247,7 +299,7 @@ public void ProcessV2_ThreadList_DestroysItsCallerOwnedStringListWhenCollectionF getOpenedProcessID=function() return pid end createStringList=function() lists=lists+1 - return {Count=0, Strings={}, destroy=function() destroyed=destroyed+1 end} + return {Count=0, destroy=function() destroyed=destroyed+1 end} end getThreadlist=function(list) error('thread enumeration failed', 0) end """); diff --git a/tests/CheatEngine.Mcp.Tests/README.md b/tests/CheatEngine.Mcp.Tests/README.md index 1d8e172..e825237 100644 --- a/tests/CheatEngine.Mcp.Tests/README.md +++ b/tests/CheatEngine.Mcp.Tests/README.md @@ -89,4 +89,9 @@ A tool guarded by a capability needs a test that proves it makes no mutating Cli Every fixed Lua script needs both a portable test and a NativeLua test. Do not treat portable tests as evidence of native Cheat Engine host compatibility. +The [full-tool qualification checklist](../../docs/qualification/all-tools/README.md) and its reviewed +per-tool evidence map distinguish routing, adapter results using doubles, direct Lua-body results and live +host results. Generate the evidence report with `eng/Write-AllToolQualificationReport.ps1` after collecting +the relevant TRX files. A catalog-wide routing pass alone does not establish that every backend tool works. + For repository-wide build, formatting, package, and contribution requirements, see [CONTRIBUTING.md](../../CONTRIBUTING.md). diff --git a/tests/CheatEngine.Mcp.Tests/Support/GatewayTestServers.cs b/tests/CheatEngine.Mcp.Tests/Support/GatewayTestServers.cs index 07bf139..4fafbac 100644 --- a/tests/CheatEngine.Mcp.Tests/Support/GatewayTestServers.cs +++ b/tests/CheatEngine.Mcp.Tests/Support/GatewayTestServers.cs @@ -163,6 +163,13 @@ internal Dictionary LastArguments get; } = new(StringComparer.Ordinal); + /// The name of the most recently forwarded tool call. + internal string? LastToolName + { + get; + private set; + } + /// The _meta of every forwarded tools/call, in arrival order; when absent. internal ConcurrentQueue ReceivedMeta { @@ -352,6 +359,7 @@ private async ValueTask ForwardAsync(CallToolRequestParams reque CancellationToken cancellationToken) { Interlocked.Increment(ref _forwardedCalls); + LastToolName = request.Name; ReceivedMeta.Enqueue(request.Meta?.DeepClone().AsObject()); lock (LastArguments) { diff --git a/tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs b/tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs index da170d7..b99c6b1 100644 --- a/tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs +++ b/tests/CheatEngine.Mcp.Tests/Tools/CodeAsmTableV2Tests.cs @@ -440,6 +440,36 @@ public void ReleasePatch_CompletedLease_ForgetsTheRetainedPatchAndResource() Assert.Empty(harness.Resources.List()); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ApplyAndCodePatch_RetainExactLeaseMetadataAndReleaseOriginalDisable(bool codePatch) + { + AutoAssemblerHarness harness = new(); + const string supplied = "[ENABLE]\nnop\n[DISABLE]\nnop"; + const string generated = "[ENABLE]\nassert(401000,8B C3)\n401000:\ndb 90 90\n\n[DISABLE]\n401000:\ndb 8B C3"; + AsmPatchApplied applied = codePatch + ? harness.Tools.ApplyCodePatch("401000", "8b c3", "90 90", "owned", Token) + : harness.Tools.Apply(supplied, "owned", Token); + try + { + Assert.Equal((true, "owned", false, false), + (applied.Retained, applied.Name, applied.AppliedAfterTargetChange, applied.RequiresManualRecovery)); + Assert.Equal(codePatch ? generated : supplied, harness.AppliedScript!.Value.Source); + Assert.Equal("owned", harness.AppliedScript.Value.Name); + Assert.Equal(new AsmPatchInfo(applied.PatchId, "owned", 1, false, true, false, false), + Assert.Single(harness.Tools.ListPatches().Patches)); + Assert.Single(harness.Resources.List()); + } + finally + { + Assert.True(harness.Tools.ReleasePatch(applied.PatchId, Token).Released); + } + Assert.Equal((1, 1), (harness.ApplyCalls, harness.ReleaseCalls)); + Assert.Empty(harness.Tools.ListPatches().Patches); + Assert.Empty(harness.Resources.List()); + } + [Fact] public void Apply_AfterTargetChange_ReportsTheActualUnretainedReleaseOutcome() { @@ -677,7 +707,20 @@ internal AutoAssemblerHarness(McpFeatureOptions? features = null, bool appliedAf { _appliedAfterTargetChange = appliedAfterTargetChange; Resources = new TargetResources(); - Tools = new AsmTools(CreateDispatch(new RecordingDispatcher(), features ?? new McpFeatureOptions()), + IInspectionClient inspection = ClientTestDouble.Create((method, arguments) => + { + Assert.Equal(nameof(IInspectionClient.TryResolveAddress), method.Name); + Assert.Equal("401000", Assert.IsType(arguments![0]).Value); + arguments[2] = new Address(0x401000); + arguments[3] = default(CheatEngineFailure); + return true; + }); + IOptions execution = Options.Create(new McpExecutionOptions()); + ToolDispatch dispatch = new(ClientTestDouble.Client(new RecordingDispatcher().Dispatcher, CancellationToken.None, + (nameof(ICheatEngineClient.Inspection), inspection)), + new McpFeatureGate(Options.Create(features ?? new McpFeatureOptions())), execution, + new DispatchStatistics(execution), TimeProvider.System, new RecordingLogger()); + Tools = new AsmTools(dispatch, Resources, ClientTestDouble.Create(AutoAssembler)); } @@ -694,6 +737,11 @@ internal int ReleaseCalls private set; } + internal AutoAssemblerScript? AppliedScript + { + get; private set; + } + internal TargetResources Resources { get; @@ -712,6 +760,7 @@ internal AsmTools Tools } ApplyCalls++; + AppliedScript = Assert.IsType(arguments![0]); return ClientTestDouble.Create(Lease); } diff --git a/tests/CheatEngine.Mcp.Tests/Tools/CodeTableQualificationTests.cs b/tests/CheatEngine.Mcp.Tests/Tools/CodeTableQualificationTests.cs new file mode 100644 index 0000000..5a491dd --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/Tools/CodeTableQualificationTests.cs @@ -0,0 +1,160 @@ +using System.Reflection; + +using CheatEngine.Client; +using CheatEngine.Client.Extensions.DependencyInjection; +using CheatEngine.Client.Tables; +using CheatEngine.Mcp.Core.Contract; +using CheatEngine.Mcp.Core.Execution; +using CheatEngine.Mcp.Core.Features; +using CheatEngine.Mcp.Core.Files; +using CheatEngine.Mcp.Tests.Core; +using CheatEngine.Mcp.Tests.Support; +using CheatEngine.Mcp.Tools.Code; +using CheatEngine.Mcp.Tools.Table; + +using Microsoft.Extensions.Options; + +namespace CheatEngine.Mcp.Tests.Tools; + +/// Managed dispatch qualification for the table-save and code-view adapters. +public sealed class CodeTableQualificationTests : IDisposable +{ + private readonly McpFilePathsTests.Scratch _scratch = new(); + + public void Dispose() => _scratch.Dispose(); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TableLoad_InspectedDataTable_ForwardsPinnedTrustedFileAndMerge(bool merge) + { + string root = _scratch.CreateFolder("tables"); + string path = Path.Combine(root, "plain.ct"); + const string content = "1Health4 Bytes
401000
"; + File.WriteAllText(path, content); + StateTestHarness lua = new(); + lua.Answer(static _ => new MonoAutoAttachState(false, true, true, true)); + int loads = 0; + ITableClient tables = ClientTestDouble.Create((method, arguments) => + { + Assert.Equal(nameof(ITableClient.LoadTrustedTable), method.Name); + TableLoadRequest request = Assert.IsType(arguments![0]); + Assert.Equal((path, merge), (request.File.FullPath, request.Merge)); + Assert.Equal(content, File.ReadAllText(request.File.FullPath)); + loads++; + return null; + }); + IOptions execution = Options.Create(new McpExecutionOptions()); + ToolDispatch dispatch = new(ClientTestDouble.Client(new RecordingDispatcher().Dispatcher, CancellationToken.None, + (nameof(ICheatEngineClient.Tables), tables)), new McpFeatureGate(Options.Create(new McpFeatureOptions())), + execution, new DispatchStatistics(execution), TimeProvider.System, new RecordingLogger(), lua.FixedLua); + CheatEngineClientOptions clientOptions = new(); + clientOptions.AllowedTableRoots.Add(root); + TableTools tools = new(dispatch, + new McpFilePaths(new McpFileOptions(), _scratch.CreateFolder("registry"), _scratch.CreateFolder("data")), + Options.Create(clientOptions)); + + TableLoadResult result = tools.Load(path, merge, TestContext.Current.CancellationToken); + + Assert.Equal((path, merge, false), (result.Path, result.Merge, result.MonoAutoAttach)); + Assert.Equal(("xml", false, false, false, 1, 0), (result.Inspection.Format, result.Inspection.ContainsLua, + result.Inspection.ContainsForms, result.Inspection.UsesMono, result.Inspection.RecordCount, + result.Inspection.AssemblerScriptCount)); + Assert.Empty(result.Inspection.Requires); + Assert.Equal(1, loads); + Assert.Equal(CheatEngineToolNames.TableLoad, Assert.Single(lua.LuaCalls).Operation); + Assert.Equal(content, File.ReadAllText(path)); + } + + [Fact] + public void TableSave_AllowedDestination_ForwardsTheTrustedPathAndReportsTheCreatedFile() + { + string root = _scratch.CreateFolder("tables"); + string destination = Path.Combine(root, "saved.ct"); + RecordingDispatcher dispatcher = new(); + ITableClient tables = ClientTestDouble.Create((method, arguments) => + { + Assert.Equal(nameof(ITableClient.SaveTable), method.Name); + PropertyInfo fileProperty = arguments![0]!.GetType().GetProperties() + .Single(static property => property.PropertyType == typeof(TrustedTableFile)); + TrustedTableFile file = Assert.IsType(fileProperty.GetValue(arguments[0])); + Assert.Equal(destination, file.FullPath); + File.WriteAllText(file.FullPath, ""); + return null; + }); + IOptions execution = Options.Create(new McpExecutionOptions()); + ToolDispatch dispatch = new(ClientTestDouble.Client(dispatcher.Dispatcher, CancellationToken.None, + (nameof(ICheatEngineClient.Tables), tables)), new McpFeatureGate(Options.Create(new McpFeatureOptions())), + execution, new DispatchStatistics(execution), TimeProvider.System, new RecordingLogger()); + CheatEngineClientOptions clientOptions = new(); + clientOptions.AllowedTableRoots.Add(root); + TableTools tools = new(dispatch, + new McpFilePaths(new McpFileOptions(), _scratch.CreateFolder("registry"), _scratch.CreateFolder("data")), + Options.Create(clientOptions)); + + TableSaveResult result = tools.Save(destination, cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal(new TableSaveResult(destination, false), result); + Assert.Equal("", File.ReadAllText(destination)); + Assert.Equal(1, dispatcher.Calls); + } + + [Fact] + public void CodeComments_ForwardOrderedExpressionsAndReturnTheFixedLuaValues() + { + StateTestHarness harness = new(); + harness.Answer(source => + { + Assert.Contains("[1] = {\"401000\",\"402000\",}", source, StringComparison.Ordinal); + return new CodeLuaComments([new CodeLuaComment("401000", "entry"), new CodeLuaComment("402000", null)]); + }); + CodeTools tools = new(harness.Dispatch, harness.Jobs); + + CodeComments result = tools.GetComments(["401000", "402000"], TestContext.Current.CancellationToken); + + Assert.Equal([new CodeComment("401000", "entry"), new CodeComment("402000", null)], result.Comments); + AssertFixedCall(harness, CheatEngineToolNames.CodeGetComments, CodeScripts.GetComments); + Assert.Equal(1, harness.Dispatches); + } + + [Fact] + public void CodeSetComment_ForwardsExactAddressAndBlankCommentToTheFixedBody() + { + StateTestHarness harness = new(); + harness.Answer(source => + { + Assert.Contains("[1] = \"401000\", [2] = \"\"", source, StringComparison.Ordinal); + return new CodeLuaComments([new CodeLuaComment("401000", string.Empty)]); + }); + CodeTools tools = new(harness.Dispatch, harness.Jobs); + + CodeComment result = tools.SetComment("401000", string.Empty, TestContext.Current.CancellationToken); + + Assert.Equal(new CodeComment("401000", string.Empty), result); + AssertFixedCall(harness, CheatEngineToolNames.CodeSetComment, CodeScripts.SetComment); + } + + [Fact] + public void CodeGetFunction_MapsKnownBoundariesAndJumpDestination() + { + StateTestHarness harness = new(); + harness.Answer(source => + { + Assert.Contains("[1] = \"401020\", [2] = 512", source, StringComparison.Ordinal); + return new CodeLuaFunction(true, "401020", "401000", "401080", 128, true); + }); + CodeTools tools = new(harness.Dispatch, harness.Jobs); + + CodeFunction result = tools.GetFunction("401020", 512, TestContext.Current.CancellationToken); + + Assert.Equal(new CodeFunction("401020", true, "401000", "401080", 128, true), result); + AssertFixedCall(harness, CheatEngineToolNames.CodeGetFunction, CodeScripts.GetFunction); + } + + private static void AssertFixedCall(StateTestHarness harness, string expectedOperation, string body) + { + (string operation, string source) = Assert.Single(harness.LuaCalls); + Assert.Equal(expectedOperation, operation); + Assert.Contains(body, source, StringComparison.Ordinal); + } +} diff --git a/tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs b/tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs new file mode 100644 index 0000000..ee3d08f --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/Tools/CoreAdapterQualificationTests.cs @@ -0,0 +1,348 @@ +using System.Collections.Immutable; +using System.Reflection; + +using CheatEngine.Client; +using CheatEngine.Client.Assembly; +using CheatEngine.Client.Inspection; +using CheatEngine.Client.Lua; +using CheatEngine.Client.Processes; +using CheatEngine.Client.Results; +using CheatEngine.Mcp.Core.Contract; +using CheatEngine.Mcp.Core.Features; +using CheatEngine.Mcp.Core.Files; +using CheatEngine.Mcp.Core.Jobs; +using CheatEngine.Mcp.Core.Values; +using CheatEngine.Mcp.Tests.Support; +using CheatEngine.Mcp.Tools.Asm; +using CheatEngine.Mcp.Tools.Code; +using CheatEngine.Mcp.Tools.Processes; +using CheatEngine.SDK.Engine.Inspection; +using CheatEngine.SDK.Engine.Runtime; +using CheatEngine.SDK.Engine.Values; + +using Microsoft.Extensions.Options; + +namespace CheatEngine.Mcp.Tests.Tools; + +/// Positive adapter checks for the remaining typed and fixed-Lua Code and Process surface. +public sealed class CoreAdapterQualificationTests +{ + private static CancellationToken Token => TestContext.Current.CancellationToken; + + [Fact] + public void Assemble_SequentialInstructions_ForwardActualOriginsAndEncodingOptions() + { + using CodeAdapterHarness harness = new(); + AsmTools tools = new(harness.Dispatch, harness.Resources); + + AsmAssembleResult result = tools.Assemble("401000", ["mov eax,ebx", "ret"], + InstructionEncodingPreference.Long, true, Token); + + Assert.Equal(("8BC3C3", 3), (result.Bytes, result.Size)); + Assert.Equal([new AsmInstruction("401000", "mov eax,ebx", "8BC3", 2), + new AsmInstruction("401002", "ret", "C3", 1)], result.Instructions); + Assert.Equal([new AssemblyInstructionRequest(new Address(0x401000), "mov eax,ebx", InstructionEncodingPreference.Long, true), + new AssemblyInstructionRequest(new Address(0x401002), "ret", InstructionEncodingPreference.Long, true)], harness.Assembled); + } + + [Fact] + public async Task DissectJob_KnownRange_PollsRetainedEventAndReleasesJob() + { + using CodeAdapterHarness harness = new(); + harness.Answer(static _ => new CodeLuaDissected("401000", 16)); + CodeJobStart started = harness.Tools.StartDissect("401000", 16, 30, Token); + ManagedJob job = harness.Jobs.Get>(started.JobId, CodeTools.JobKind); + try + { + await job.Completion.WaitAsync(TimeSpan.FromSeconds(10), Token); + CodeJobPage page = harness.Tools.PollJob(started.JobId, 0, 1); + Assert.True(page.Job.State == JobState.Completed, + page.Job.Error + string.Join('\n', harness.Log.Entries.Select(static entry => entry.Exception?.ToString()))); + Assert.Equal(new CodeJobEvent("dissect", "401000", null, 16), Assert.Single(page.Items)); + Assert.Equal((JobState.Completed, 16L, 16L, 1L, false, 0L), + (page.Job.State, page.Job.ProgressDone, page.Job.ProgressTotal, page.NextAfterSequence, page.More, page.Dropped)); + Assert.Equal(page.Items, harness.Tools.PollJob(started.JobId, 0, 1).Items); + Assert.Empty(harness.Tools.PollJob(started.JobId, page.NextAfterSequence, 1).Items); + } + finally + { + Assert.True(harness.Jobs.Stop(started.JobId, Token).Released); + } + Assert.Empty(harness.Resources.List()); + Assert.Equal(0, harness.Jobs.Count); + Assert.Equal([CheatEngineToolNames.CodeStartDissect], harness.LuaOperations); + Assert.Contains("[1] = 0x401000, [2] = 16", harness.LuaSources[0], StringComparison.Ordinal); + } + + [Fact] + public void DecodeAndDisassemble_MapTypedInstructionsInRequestedOrder() + { + using CodeAdapterHarness harness = new(); + Queue columns = new( + [ + new CodeLuaDisassemblyColumns("401000", "mov eax,ebx", ""), + new CodeLuaDisassemblyColumns("401000", "mov eax,ebx", ""), + new CodeLuaDisassemblyColumns("401002", "ret", "") + ]); + harness.Answer(_ => columns.Dequeue()); + harness.AddInstruction(0x401000, "mov eax,ebx", [0x8B, 0xC3]); + harness.AddInstruction(0x401002, "ret", [0xC3]); + CodeTools tools = harness.Tools; + + CodeDecodeResult decoded = tools.Decode("401000", Token); + CodeDisassembly disassembly = tools.Disassemble("401000", 2, cancellationToken: Token); + + Assert.Equal(("401000", "mov eax,ebx", "8BC3", 2), + (decoded.Instruction.Address, decoded.Instruction.Opcode, decoded.Instruction.Bytes, decoded.Length)); + Assert.Equal(["401000", "401002"], disassembly.Instructions.Select(static item => item.Address)); + Assert.Equal(["8BC3", "C3"], disassembly.Instructions.Select(static item => item.Bytes)); + Assert.Equal([0x401000UL, 0x401000UL, 0x401002UL], harness.Disassembled); + Assert.Empty(columns); + } + + [Fact] + public void DissectorViews_MapFixedLuaPagesAndClearSideEffect() + { + using CodeAdapterHarness harness = new(); + harness.Answer(_ => new CodeLuaReferencePage("5000", 2, true, + [new CodeLuaReference("401000", "5000", "call")], 1)); + harness.Answer(_ => new CodeLuaStringPage(2, true, + [new CodeLuaString("6000", "Health")], 1)); + harness.Answer(_ => new CodeLuaFunctionPage(2, ["401000"], 1)); + harness.Answer(_ => new CodeLuaCleared(true)); + + CodeReferencePage references = harness.Tools.FindReferences("5000", 1, 1, Token); + CodeStringPage strings = harness.Tools.FindStrings("hea", 1, 1, Token); + CodeFunctionPage functions = harness.Tools.ListFunctions(1, 1, Token); + CodeClearResult cleared = harness.Tools.ClearDissect(Token); + + Assert.Equal(("5000", 2, true, 1, 1), + (references.Address, references.Total, references.Exact, references.Offset, references.NextOffset)); + Assert.Equal(new CodeReference("401000", "5000", "call"), Assert.Single(references.References)); + Assert.Equal((2, true, 1, "6000", "Health"), + (strings.Total, strings.Exact, strings.Offset, strings.Strings[0].Address, strings.Strings[0].Text)); + Assert.Equal((2, 1, "401000", 1), + (functions.Total, functions.Offset, Assert.Single(functions.Functions), functions.NextOffset)); + Assert.True(cleared.Cleared); + Assert.Equal( + [ + CheatEngineToolNames.CodeFindReferences, CheatEngineToolNames.CodeFindStrings, + CheatEngineToolNames.CodeListFunctions, CheatEngineToolNames.CodeClearDissect + ], harness.LuaOperations); + } + + [Fact] + public void ProcessListCurrentAndAttach_ForwardTypedRequestsAndMapResults() + { + using ProcessAdapterHarness harness = new(); + harness.Current = Process(7, "old.exe", 4, 2); + harness.Local = new LocalProcessEnumerationResult( + ImmutableArray.Create(new LocalProcessSnapshot(new LocalProcessId(11), "alpha.exe", null), + new LocalProcessSnapshot(new LocalProcessId(12), "beta.exe", null)), true); + harness.Attached = Process(12, "beta.exe", 8, 3); + + ProcessListResult listed = harness.Tools.List(2, "beta", Token); + ProcessCurrentResult current = harness.Tools.GetCurrent(Token); + ProcessAttachResult attached = harness.Tools.Attach("12", Token); + + Assert.Equal([new ProcessSummary(11, "alpha.exe"), new ProcessSummary(12, "beta.exe")], listed.Processes); + Assert.True(listed.Truncated); + Assert.Equal(new ProcessCurrentResult(true, 7, "old.exe", 4, 2), current); + Assert.Equal(new ProcessAttachResult(12, "beta.exe", false), attached); + Assert.Equal(new LocalProcessEnumerationRequest(2, "beta"), Assert.Single(harness.ListRequests)); + Assert.Equal([nameof(IProcessClient.GetLocalProcesses), nameof(IProcessClient.TryGetCurrentProcess), + nameof(IProcessClient.TryGetCurrentProcess), nameof(IProcessClient.Attach)], harness.Calls); + } + + [Fact] + public void ProcessGetCurrent_Unattached_MapsTheNormalEmptyState() + { + using ProcessAdapterHarness harness = new() + { + HasCurrent = false + }; + + ProcessCurrentResult current = harness.Tools.GetCurrent(Token); + + Assert.Equal(new ProcessCurrentResult(false), current); + Assert.Equal([nameof(IProcessClient.TryGetCurrentProcess)], harness.Calls); + } + + private sealed class CodeAdapterHarness : IDisposable + { + private readonly StateTestHarness _lua = new(); + private readonly Dictionary _instructions = []; + + internal CodeAdapterHarness() + { + IAssemblyClient assembly = ClientTestDouble.Create(Assembly); + IInspectionClient inspection = ClientTestDouble.Create(Inspection); + IProcessClient processes = ClientTestDouble.Create((method, _) => + { + Assert.Equal(nameof(IProcessClient.GetCurrentProcess), method.Name); + return Process(7, "owned.exe", 8, 1); + }); + ICheatEngineClient client = ClientTestDouble.Client(new RecordingDispatcher().Dispatcher, CancellationToken.None, + (nameof(ICheatEngineClient.Assembly), assembly), (nameof(ICheatEngineClient.Inspection), inspection), + (nameof(ICheatEngineClient.Processes), processes)); + IOptions options = Options.Create(new McpExecutionOptions()); + ToolDispatch dispatch = new(client, new McpFeatureGate(Options.Create(new McpFeatureOptions())), options, + new DispatchStatistics(options), TimeProvider.System, Log, _lua.FixedLua); + Dispatch = dispatch; + Resources = new TargetResources(); + Jobs = new JobRegistry(dispatch, Resources, options, TimeProvider.System); + Tools = new CodeTools(dispatch, Jobs); + } + + internal TargetResources Resources + { + get; + } + internal ToolDispatch Dispatch + { + get; + } + internal JobRegistry Jobs + { + get; + } + internal CodeTools Tools + { + get; + } + internal List Disassembled { get; } = []; + internal RecordingLogger Log { get; } = new(); + internal List Assembled { get; } = []; + internal string[] LuaOperations => [.. _lua.LuaCalls.Select(static call => call.Operation)]; + internal string[] LuaSources => [.. _lua.LuaCalls.Select(static call => call.Source)]; + + internal void Answer(Func answer) => _lua.Answer(answer); + internal void AddInstruction(ulong address, string opcode, byte[] bytes) => _instructions[address] = (opcode, bytes); + + private object? Inspection(MethodInfo method, object?[]? arguments) + { + Assert.Equal(nameof(IInspectionClient.TryResolveAddress), method.Name); + object?[] values = arguments!; + Assert.True(HexParse.TryAddress(((SymbolExpression) values[0]!).Value, out ulong address)); + values[2] = new Address(address); + values[3] = default(CheatEngineFailure); + return true; + } + + private object? Assembly(MethodInfo method, object?[]? arguments) + { + if (method.Name == nameof(IAssemblyClient.Assemble)) + { + AssemblyInstructionRequest request = Assert.IsType(arguments![0]); + Assembled.Add(request); + return request.Instruction == "ret" ? ImmutableArray.Create((byte) 0xC3) + : ImmutableArray.Create((byte) 0x8B, (byte) 0xC3); + } + Assert.Equal(nameof(IAssemblyClient.Disassemble), method.Name); + object?[] values = arguments!; + ulong address = ((Address) values[0]!).ToUInt64(); + Disassembled.Add(address); + (string opcode, byte[] bytes) = _instructions[address]; + return new AssemblyInstructionSnapshot(new Address(address), bytes.Length, HexFormat.Address(address), opcode, + string.Empty, bytes); + } + + public void Dispose() + { + Jobs.Dispose(); + _lua.Stopping.Dispose(); + } + } + + private sealed class ProcessAdapterHarness : IDisposable + { + private readonly StateTestHarness _lua = new(); + + internal ProcessAdapterHarness() + { + IProcessClient processes = ClientTestDouble.Create(Process); + ICheatEngineClient client = ClientTestDouble.Client(new RecordingDispatcher().Dispatcher, CancellationToken.None, + (nameof(ICheatEngineClient.Processes), processes)); + IOptions options = Options.Create(new McpExecutionOptions()); + ToolDispatch dispatch = new(client, new McpFeatureGate(Options.Create(new McpFeatureOptions + { + EnableTargetCodeExecution = true + })), options, new DispatchStatistics(options), TimeProvider.System, new RecordingLogger(), + _lua.FixedLua); + Resources = new TargetResources(); + Tools = new ProcessTools(dispatch, Resources, new TargetTransitionGuards([]), Files()); + _lua.Answer(_ => new MonoAutoAttachState(false, false, false, true)); + } + + internal ProcessTools Tools + { + get; + } + internal TargetResources Resources + { + get; + } + internal ProcessSnapshot Current + { + get; set; + } + internal ProcessSnapshot Attached + { + get; set; + } + internal LocalProcessEnumerationResult Local + { + get; set; + } + internal bool HasCurrent { get; set; } = true; + internal List Calls { get; } = []; + internal List ListRequests { get; } = []; + + private object? Process(MethodInfo method, object?[]? arguments) + { + object?[] values = arguments!; + Calls.Add(method.Name); + switch (method.Name) + { + case nameof(IProcessClient.GetLocalProcesses): + ListRequests.Add((LocalProcessEnumerationRequest) values[0]!); + return Local; + case nameof(IProcessClient.TryGetCurrentProcess): + if (!HasCurrent) + { + values[0] = default(ProcessSnapshot); + values[1] = new CheatEngineFailure(CheatEngineFailureKind.TargetNotAttached, + "Processes.GetCurrentProcess", "No process is selected.", + hostEffect: CheatEngineHostEffect.NotStarted); + return false; + } + + values[0] = Current; + values[1] = default(CheatEngineFailure); + return true; + case nameof(IProcessClient.Attach): + Assert.Equal(12, ((TargetProcessId) values[0]!).Value); + return Attached; + default: + throw new Xunit.Sdk.XunitException($"Unexpected process call {method.Name}."); + } + } + + private static McpFilePaths Files() + { + string root = Path.GetTempPath(); + return new McpFilePaths(new McpFileOptions { AllowedRoots = [root] }, Path.Combine(root, "ce-mcp-registry"), + Path.Combine(root, "ce-mcp-data")); + } + + public void Dispose() + { + _lua.Stopping.Dispose(); + } + } + + private static ProcessSnapshot Process(int id, string name, int pointerSize, long epoch) => + new(new TargetProcessId(id), name, null, TargetBackend.LocalProcess, + pointerSize == 4 ? CheatEngineArchitecture.X86 : CheatEngineArchitecture.X64, + pointerSize == 4 ? PointerSize.Bit32 : PointerSize.Bit64, pointerSize, null, epoch); +} diff --git a/tests/CheatEngine.Mcp.Tests/Tools/Exec/ExecSupportTests.cs b/tests/CheatEngine.Mcp.Tests/Tools/Exec/ExecSupportTests.cs new file mode 100644 index 0000000..6a4ec75 --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/Tools/Exec/ExecSupportTests.cs @@ -0,0 +1,178 @@ +using CheatEngine.Client; +using CheatEngine.Client.Dispatching; +using CheatEngine.Client.Inspection; +using CheatEngine.Client.Results; +using CheatEngine.Mcp.Core.Contract; +using CheatEngine.Mcp.Tests.Support; +using CheatEngine.Mcp.Tools.Exec; +using CheatEngine.SDK.Engine.Inspection; +using CheatEngine.SDK.Engine.Values; + +namespace CheatEngine.Mcp.Tests.Tools.Exec; + +/// Execution inputs preserve native numeric types and fail before any target effect. +public sealed class ExecSupportTests +{ + [Fact] + public void Arguments_KnownNumericBoundaries_PreserveExactValuesAndNativeTypes() + { + (int[] types, object?[] values) = ExecSupport.Arguments( + [ + new(ExecArgumentType.Integral, "-9223372036854775808"), + new(ExecArgumentType.Integral, "18446744073709551615"), + new(ExecArgumentType.Integral, " 0X8000000000000000 "), + new(ExecArgumentType.Integral, "FFFFFFFFFFFFFFFF"), + new(ExecArgumentType.SinglePrecision, "1.25"), + new(ExecArgumentType.DoublePrecision, "-2.5e10") + ]); + + Assert.Equal([0, 0, 0, 0, 1, 2], types); + Assert.Equal(long.MinValue, Assert.IsType(values[0])); + Assert.Equal(ulong.MaxValue, Assert.IsType(values[1])); + Assert.Equal(0x8000000000000000UL, Assert.IsType(values[2])); + Assert.Equal(ulong.MaxValue, Assert.IsType(values[3])); + Assert.Equal(1.25f, Assert.IsType(values[4])); + Assert.Equal(-25_000_000_000d, Assert.IsType(values[5])); + } + + [Fact] + public void Arguments_EmptyList_ProducesEmptyParallelNativeArrays() + { + (int[] types, object?[] values) = ExecSupport.Arguments([]); + + Assert.Empty(types); + Assert.Empty(values); + } + + [Fact] + public void Arguments_NullListOrEntry_ReportsTheOriginalArgumentIndex() + { + AssertInvalid(() => ExecSupport.Arguments(null), "arguments:"); + AssertInvalid(() => ExecSupport.Arguments([new(ExecArgumentType.Integral, "1"), null!]), "arguments[1]:"); + } + + [Theory] + [InlineData("missing")] + [InlineData("too_long")] + public void Arguments_MissingOrOversizedText_ReportsIndexedValue(string caseId) + { + string text = caseId == "missing" ? null! : new string(' ', ExecSupport.MaximumArgumentText) + "1"; + + AssertInvalid(() => ExecSupport.Arguments([new(ExecArgumentType.Integral, text)]), "arguments[0].value:"); + } + + [Fact] + public void Arguments_ExactMaximumTextLength_AcceptsAndPreservesTheParsedValue() + { + string text = new string(' ', ExecSupport.MaximumArgumentText - 1) + "1"; + + (int[] types, object?[] values) = ExecSupport.Arguments([new(ExecArgumentType.Integral, text)]); + + Assert.Equal([0], types); + Assert.Equal(1L, Assert.IsType(Assert.Single(values))); + } + + [Theory] + [InlineData("empty")] + [InlineData("prefix_only")] + [InlineData("non_hex")] + [InlineData("decimal_overflow")] + [InlineData("hex_overflow")] + public void Arguments_InvalidIntegerPartition_RefusesWithoutNarrowing(string caseId) + { + string value = caseId switch + { + "empty" => string.Empty, + "prefix_only" => "0x", + "non_hex" => "7FFG", + "decimal_overflow" => "18446744073709551616", + _ => "0x10000000000000000" + }; + + AssertInvalid(() => ExecSupport.Arguments([new(ExecArgumentType.Integral, value)]), "arguments[0].value:"); + } + + [Theory] + [InlineData(ExecArgumentType.SinglePrecision, "NaN")] + [InlineData(ExecArgumentType.SinglePrecision, "Infinity")] + [InlineData(ExecArgumentType.SinglePrecision, "1e1000")] + [InlineData(ExecArgumentType.DoublePrecision, "NaN")] + [InlineData(ExecArgumentType.DoublePrecision, "-Infinity")] + [InlineData(ExecArgumentType.DoublePrecision, "1e1000")] + public void Arguments_NonFiniteFloatingPoint_RefusesBeforeInvocation(ExecArgumentType type, string value) + { + AssertInvalid(() => ExecSupport.Arguments([new(type, value)]), "arguments[0].value:"); + } + + [Theory] + [InlineData(1)] + [InlineData(10_000)] + public void Timeout_ExactSupportedEndpoints_AreAccepted(int milliseconds) + { + Assert.Null(Xunit.Record.Exception(() => ExecSupport.Timeout(milliseconds))); + } + + [Theory] + [InlineData(0)] + [InlineData(10_001)] + public void Timeout_AdjacentUnsupportedValues_RefuseBeforeInvocation(int milliseconds) + { + AssertInvalid(() => ExecSupport.Timeout(milliseconds), "timeoutMilliseconds:"); + } + + [Theory] + [InlineData(CheatEngineFailureKind.NotFound, false, ToolErrorKind.NotFound)] + [InlineData(CheatEngineFailureKind.Cancelled, false, ToolErrorKind.Cancelled)] + [InlineData(CheatEngineFailureKind.Cancelled, true, ToolErrorKind.Stopping)] + [InlineData(CheatEngineFailureKind.InvalidState, true, ToolErrorKind.Stopping)] + [InlineData(CheatEngineFailureKind.LuaError, false, ToolErrorKind.HostRefused)] + public void Resolve_FailedLookup_PreservesFailureAndActivationStoppingSemantics( + CheatEngineFailureKind failureKind, bool stopping, ToolErrorKind expected) + { + using CancellationTokenSource lifetime = new(); + if (stopping) + { + lifetime.Cancel(); + } + + int lookups = 0; + IInspectionClient inspection = ClientTestDouble.Create((method, arguments) => + { + Assert.Equal(nameof(IInspectionClient.TryResolveAddress), method.Name); + Assert.Equal("missing", Assert.IsType(arguments![0]).Value); + arguments[2] = default(Address); + arguments[3] = new CheatEngineFailure(failureKind, "Inspection.ResolveAddress", "known lookup refusal", + hostEffect: CheatEngineHostEffect.NotStarted); + lookups++; + return false; + }); + ICheatEngineDispatcher dispatcher = ClientTestDouble.Create(static (_, _) => + throw new InvalidOperationException("Address failure must not invoke target code.")); + ICheatEngineClient client = ClientTestDouble.Client(dispatcher, lifetime.Token, + (nameof(ICheatEngineClient.Inspection), inspection)); + + CheatEngineToolException exception = Assert.Throws(() => + ExecSupport.Resolve(client, "missing", "address", TestContext.Current.CancellationToken)); + + Assert.Equal(1, lookups); + Assert.Equal((expected, ToolHostEffect.NotStarted), (exception.Error.Kind, exception.Error.HostEffect)); + if (failureKind == CheatEngineFailureKind.NotFound) + { + Assert.Equal("address does not resolve to an address.", exception.Error.Message); + Assert.Equal("Check the expression with symbol_resolve.", exception.Error.Hint); + } + else + { + Assert.Equal("Inspection.ResolveAddress", exception.Error.Operation); + Assert.Equal("known lookup refusal", exception.Error.Message); + } + } + + private static void AssertInvalid(Action action, string parameterPrefix) + { + CheatEngineToolException exception = Assert.Throws(action); + Assert.Equal((ToolErrorKind.InvalidArgument, ToolHostEffect.NotStarted, false), + (exception.Error.Kind, exception.Error.HostEffect, exception.Error.Retryable)); + Assert.StartsWith(parameterPrefix, exception.Error.Message, StringComparison.Ordinal); + } +} diff --git a/tests/CheatEngine.Mcp.Tests/Tools/Lua/LuaToolQualificationTests.cs b/tests/CheatEngine.Mcp.Tests/Tools/Lua/LuaToolQualificationTests.cs new file mode 100644 index 0000000..9968d4a --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/Tools/Lua/LuaToolQualificationTests.cs @@ -0,0 +1,199 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +using CheatEngine.Client; +using CheatEngine.Client.Lua; +using CheatEngine.Client.Results; +using CheatEngine.Mcp.Core.Contract; +using CheatEngine.Mcp.Core.Execution; +using CheatEngine.Mcp.Core.Features; +using CheatEngine.Mcp.Core.Lua; +using CheatEngine.Mcp.Tests.Support; +using CheatEngine.Mcp.Tools.Lua; + +using Microsoft.Extensions.Options; + +using ToolLuaExecuteOutcome = CheatEngine.Mcp.Tools.Lua.LuaExecuteOutcome; + +namespace CheatEngine.Mcp.Tests.Tools.Lua; + +/// +/// Managed-adapter qualification for the two advertised Lua tools. Fixed Lua's native behavior is covered +/// separately; these cases prove the tool-to-dispatch and unsafe-adapter contracts without Cheat Engine. +/// +public sealed class LuaToolQualificationTests +{ + [Fact] + public void FindApi_ForwardsExactPageArgumentsAndReturnsEveryKnownPageField() + { + StateTestHarness harness = new(); + LuaApiSearchResult expected = new(12, 10, false, + [ + new LuaApiSearchLine(41, "getAddressSafe(address)"), + new LuaApiSearchLine(59, "getAddressList().getMemoryRecordByDescription(name)") + ]); + harness.Answer(source => + { + Assert.Contains("local a = { n = 3, [1] = \"getAddress\", [2] = 2, [3] = 8 };", source, + StringComparison.Ordinal); + return expected; + }); + LuaTools tools = new(harness.Dispatch); + + LuaApiSearchResult result = tools.FindApi("getAddress", 2, 8, TestContext.Current.CancellationToken); + + Assert.Equal((12, 10, false), (result.Total, result.NextOffset, result.Truncated)); + Assert.Equal([(41, "getAddressSafe(address)"), + (59, "getAddressList().getMemoryRecordByDescription(name)")], + result.Lines.Select(static line => (line.Number, line.Text))); + (string operation, string source) = Assert.Single(harness.LuaCalls); + Assert.Equal(CheatEngineToolNames.LuaFindApi, operation); + Assert.Contains(LuaScripts.FindApi, source, StringComparison.Ordinal); + Assert.Equal(1, harness.Dispatches); + } + + [Fact] + public void FindApi_MaximumValidUtf8Query_ReachesTheFixedLuaAdapter() + { + StateTestHarness harness = new(); + string query = new('\u00e9', LuaTools.MaximumQueryBytes / 2); + harness.Answer(source => + { + Assert.Contains("[1] = \"\\195\\169", source, StringComparison.Ordinal); + return new LuaApiSearchResult(0, null, false, []); + }); + LuaTools tools = new(harness.Dispatch); + + LuaApiSearchResult result = tools.FindApi(query, 1, 0, TestContext.Current.CancellationToken); + + Assert.Equal((0, null, false), (result.Total, result.NextOffset, result.Truncated)); + Assert.Single(harness.LuaCalls); + Assert.Equal(1, harness.Dispatches); + } + + [Fact] + public void FindApi_UnpairedSurrogate_RefusesBeforeFixedLuaDispatch() + { + StateTestHarness harness = new(); + LuaTools tools = new(harness.Dispatch); + + CheatEngineToolException exception = Assert.Throws(() => + tools.FindApi("get\uD800Address", cancellationToken: TestContext.Current.CancellationToken)); + + Assert.Equal((ToolErrorKind.InvalidArgument, ToolHostEffect.NotStarted), + (exception.Error.Kind, exception.Error.HostEffect)); + Assert.Contains("unpaired UTF-16 surrogate", exception.Error.Message, StringComparison.Ordinal); + Assert.Empty(harness.LuaCalls); + Assert.Equal(0, harness.Dispatches); + } + + [Fact] + public void Execute_DisabledFeature_RefusesBeforeUnsafeOrFixedLua() + { + FixedLuaDouble fixedLua = new(new ToolLuaExecuteOutcome(true)); + int unsafeCalls = 0; + IUnsafeLuaClient unsafeLua = CreateUnsafeLua(() => unsafeCalls++); + LuaTools tools = new(CreateDispatch(fixedLua, enableUnsafeLua: false), unsafeLua); + + CheatEngineToolException exception = Assert.Throws(() => + tools.Execute("return 7", cancellationToken: TestContext.Current.CancellationToken)); + + Assert.Equal((ToolErrorKind.CapabilityDisabled, ToolHostEffect.NotStarted), + (exception.Error.Kind, exception.Error.HostEffect)); + Assert.Equal(0, unsafeCalls); + Assert.Empty(fixedLua.Calls); + } + + [Fact] + public void Execute_MissingUnsafeClient_ReportsUnsupportedBeforeFixedLua() + { + FixedLuaDouble fixedLua = new(new ToolLuaExecuteOutcome(true)); + LuaTools tools = new(CreateDispatch(fixedLua, enableUnsafeLua: true)); + + CheatEngineToolException exception = Assert.Throws(() => + tools.Execute("return 7", cancellationToken: TestContext.Current.CancellationToken)); + + Assert.Equal((ToolErrorKind.Unsupported, ToolHostEffect.NotStarted), + (exception.Error.Kind, exception.Error.HostEffect)); + Assert.Empty(fixedLua.Calls); + } + + [Fact] + public void Execute_Success_UsesTheUnsafeAdapterAndKeepsCopiedValuesAndOpaqueCount() + { + JsonElement[] values = + [ + JsonElement.Parse("7"), + JsonElement.Parse("{\"visible\":true}") + ]; + FixedLuaDouble fixedLua = new(new ToolLuaExecuteOutcome(true, ReturnValues: values), droppedOpaqueCount: 3); + int unsafeCalls = 0; + LuaTools tools = new(CreateDispatch(fixedLua, enableUnsafeLua: true), CreateUnsafeLua(() => unsafeCalls++)); + + LuaExecuteResult result = tools.Execute("return 7, print, { visible = true }", "=qualification", + TestContext.Current.CancellationToken); + + Assert.Equal((true, null, null, ToolHostEffect.Completed, 3), + (result.Ok, result.Phase, result.Error, result.HostEffect, result.DroppedOpaqueCount)); + Assert.Equal(7, result.ReturnValues![0].GetInt32()); + Assert.True(result.ReturnValues[1].GetProperty("visible").GetBoolean()); + Assert.Equal(1, unsafeCalls); + Assert.Single(fixedLua.Calls); + Assert.Equal(CheatEngineToolNames.LuaExecute, fixedLua.Calls[0].Operation); + } + + [Theory] + [InlineData("compile", ToolHostEffect.NotApplied)] + [InlineData("runtime", ToolHostEffect.Unknown)] + public void Execute_FailedLuaOutcome_MapsPhaseToTheAdvertisedHostEffect(string phase, ToolHostEffect effect) + { + FixedLuaDouble fixedLua = new(new ToolLuaExecuteOutcome(false, phase, "simulated " + phase + " failure")); + int unsafeCalls = 0; + LuaTools tools = new(CreateDispatch(fixedLua, enableUnsafeLua: true), CreateUnsafeLua(() => unsafeCalls++)); + + LuaExecuteResult result = tools.Execute("return 7", cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal((false, phase, "simulated " + phase + " failure", effect, null, 0), + (result.Ok, result.Phase, result.Error, result.HostEffect, result.ReturnValues, result.DroppedOpaqueCount)); + Assert.Equal(1, unsafeCalls); + Assert.Single(fixedLua.Calls); + } + + private static ToolDispatch CreateDispatch(IFixedLuaExecutor fixedLua, bool enableUnsafeLua) + { + IOptions execution = Options.Create(new McpExecutionOptions()); + return new ToolDispatch(ClientTestDouble.Client(), + new McpFeatureGate(Options.Create(new McpFeatureOptions { EnableUnsafeLua = enableUnsafeLua })), execution, + new DispatchStatistics(execution), TimeProvider.System, new RecordingLogger(), fixedLua); + } + + private static IUnsafeLuaClient CreateUnsafeLua(Action invoked) + { + return ClientTestDouble.Create((method, arguments) => + { + Assert.Equal(nameof(IUnsafeLuaClient.TryExecute), method.Name); + LuaScript script = Assert.IsType(arguments![0]); + Assert.Contains("return 7", script.Source, StringComparison.Ordinal); + invoked(); + arguments[1] = default(CheatEngineFailure); + return true; + }); + } + + private sealed class FixedLuaDouble(object result, int droppedOpaqueCount = 0) : IFixedLuaExecutor + { + internal List<(string Operation, string Source)> Calls + { + get; + } = []; + + public LuaJsonResult Execute(string operation, string source, JsonTypeInfo resultType, + LuaJsonBufferPool buffers, LuaOpaqueValueHandling opaque, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + Calls.Add((operation, source)); + Assert.Equal(LuaOpaqueValueHandling.Drop, opaque); + return new LuaJsonResult(Assert.IsType(result), null, droppedOpaqueCount); + } + } +} diff --git a/tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs b/tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs new file mode 100644 index 0000000..f50d78b --- /dev/null +++ b/tests/CheatEngine.Mcp.Tests/Tools/ManagedAdapterQualificationTests.cs @@ -0,0 +1,208 @@ +using System.Reflection; + +using CheatEngine.Mcp.Core.Contract; +using CheatEngine.Mcp.Core.Features; +using CheatEngine.Mcp.Tests.Support; +using CheatEngine.Mcp.Tools.DotNet; +using CheatEngine.Mcp.Tools.Mono; + +namespace CheatEngine.Mcp.Tests.Tools; + +/// Positive managed-adapter checks for the .NET and Mono collector surfaces. +public sealed class ManagedAdapterQualificationTests +{ + private static CancellationToken Token => TestContext.Current.CancellationToken; + + [Fact] + public void DotNetDiscoveryAdapters_MapStatusDomainsAndModulesWithRequestedPage() + { + StateTestHarness harness = new(); + harness.Answer(static _ => new DotNetStatus(true, true, 2)); + harness.Answer(static _ => new DotNetDomainPage([new DotNetDomain("7", "game")], 2, 1)); + harness.Answer(static _ => new DotNetModulePage("7", + [new DotNetModule("11", "Game.dll")], 1)); + DotNetTools tools = DotNet(harness); + + DotNetStatus status = tools.GetStatus(Token); + DotNetDomainPage domains = tools.ListDomains(0, 1, Token); + DotNetModulePage modules = tools.ListModules(" 7 ", 0, 1, Token); + + Assert.Equal((true, true, 2), (status.Available, status.Attached, status.DomainCount)); + Assert.Equal(("7", "game", 2, 1), (domains.Domains[0].Handle, domains.Domains[0].Name, domains.Total, + domains.NextOffset)); + Assert.Equal(("7", "11", "Game.dll", 1), (modules.DomainHandle, modules.Modules[0].Handle, + modules.Modules[0].Name, modules.Total)); + AssertOperations(harness, + (CheatEngineToolNames.DotNetGetStatus, null), + (CheatEngineToolNames.DotNetListDomains, "[1] = 0, [2] = 1"), + (CheatEngineToolNames.DotNetListModules, "[1] = \"7\", [2] = 0, [3] = 1")); + } + + [Fact] + public void DotNetTypeAdapters_MapPagesDetailsAndMethodsWithFilters() + { + StateTestHarness harness = new(); + harness.Answer(static _ => new DotNetTypePage("11", + [new DotNetType("33554433", "Game.Player", 1, "33554432")], 3, 2)); + harness.Answer(static _ => new DotNetTypeDetails("11", "33554433", "Game.Player", + "System.Object", 18, null, null, null, null, [new DotNetField("health", 16, FieldType: "System.Int32", + ElementType: 8)])); + harness.Answer(static _ => new DotNetMethodPage("11", "33554433", + [new DotNetMethod("100663297", "Damage", NativeCode: "7FF01000")], 1)); + DotNetTools tools = DotNet(harness); + + DotNetTypePage types = tools.ListTypes(" 11 ", 1, 1, "Player", Token); + DotNetTypeDetails details = tools.GetType(" 11 ", " 33554433 ", 1, Token); + DotNetMethodPage methods = tools.ListMethods(" 11 ", " 33554433 ", 0, 1, "Damage", Token); + + Assert.Equal(("11", "33554433", "Game.Player", 3, 2), (types.ModuleHandle, types.Types[0].Token, + types.Types[0].Name, types.Total, types.NextOffset)); + Assert.Equal(("Game.Player", "System.Object", "health", 16L, 8L), (details.Name, details.BaseType, + details.Fields[0].Name, details.Fields[0].Offset, details.Fields[0].ElementType)); + Assert.Equal(("100663297", "Damage", "7FF01000", 1), (methods.Methods[0].Token, methods.Methods[0].Name, + methods.Methods[0].NativeCode, methods.Total)); + AssertOperations(harness, + (CheatEngineToolNames.DotNetListTypes, "[1] = \"11\", [2] = 1, [3] = 1, [4] = \"Player\""), + (CheatEngineToolNames.DotNetGetType, "[1] = \"11\", [2] = \"33554433\", [3] = 1"), + (CheatEngineToolNames.DotNetListMethods, + "[1] = \"11\", [2] = \"33554433\", [3] = 0, [4] = 1, [5] = \"Damage\"")); + } + + [Fact] + public void DotNetParameterAndObjectAdapters_MapKnownFieldsAndNormalizedInputs() + { + StateTestHarness harness = new(); + harness.Answer(static _ => new DotNetMethodParameters("11", "100663297", + [new DotNetParameter(0, "amount", 0x08)], "System.Void (System.Int32)")); + harness.Answer(static _ => new DotNetObject("7FF02000", "Game.Player", + [new DotNetObjectField("health", "100", "System.Int32", 16, 8)])); + DotNetTools tools = DotNet(harness); + + DotNetMethodParameters parameters = tools.GetMethodParameters(" 11 ", " 100663297 ", Token); + DotNetObject value = tools.GetObject(" [player]+10 ", 1, Token); + + Assert.Equal(("11", "100663297", "amount", "Int32", "System.Void (System.Int32)"), + (parameters.ModuleHandle, parameters.MethodToken, parameters.Parameters[0].Name, + parameters.Parameters[0].ElementTypeName, parameters.Signature)); + Assert.Equal(("7FF02000", "Game.Player", "health", "100", 16L), (value.Address, value.TypeName, + value.Fields[0].Name, value.Fields[0].Value, value.Fields[0].Offset)); + AssertOperations(harness, + (CheatEngineToolNames.DotNetGetMethodParameters, "[1] = \"11\", [2] = \"100663297\""), + (CheatEngineToolNames.DotNetGetObject, "[1] = \"[player]+10\", [2] = 1")); + } + + [Fact] + public void MonoDiscoveryAdapters_MapStatusAndPagedCollectorResults() + { + StateTestHarness harness = new(); + harness.Answer(static _ => new MonoStatus(true, false, 7, ["3"])); + harness.Answer(static _ => new MonoAssemblyPage([new MonoAssembly("5", "6", "Game")], 2, 1)); + harness.Answer(static _ => new MonoClassPage("6", [new MonoClass("9", "Player", "Game")], 1)); + harness.Answer(static _ => new MonoFieldList("9", + [new MonoField("10", "health", "System.Int32", 16, false)])); + harness.Answer(static _ => new MonoMethodPage("9", + [new MonoMethod("11", "Damage", "System.Int32", "System.Void", ["amount"], 0x10, true)], 1)); + MonoTools tools = Mono(harness); + + MonoStatus status = tools.GetStatus(Token); + MonoAssemblyPage assemblies = tools.ListAssemblies(0, 1, Token); + MonoClassPage classes = tools.ListClasses(" 6 ", 0, 1, "Player", Token); + MonoFieldList fields = tools.ListFields(" 9 ", false, 1, "health", Token); + MonoMethodPage methods = tools.ListMethods(" 9 ", 0, 1, "Damage", Token); + + Assert.Equal((true, 7L, "3"), (status.Attached, status.CollectorVersion, Assert.Single(status.Domains!))); + Assert.Equal(("5", "6", "Game", 2, 1), (assemblies.Assemblies[0].AssemblyHandle, + assemblies.Assemblies[0].ImageHandle, assemblies.Assemblies[0].Name, assemblies.Total, assemblies.NextOffset)); + Assert.Equal(("6", "9", "Player", "Game"), (classes.ImageHandle, classes.Classes[0].Handle, + classes.Classes[0].Name, classes.Classes[0].Namespace)); + Assert.Equal(("9", "10", "health", 16L), (fields.ClassHandle, fields.Fields[0].Handle, + fields.Fields[0].Name, fields.Fields[0].Offset)); + Assert.Equal(("9", "11", "Damage", true), (methods.ClassHandle, methods.Methods[0].Handle, + methods.Methods[0].Name, methods.Methods[0].IsStatic)); + AssertOperations(harness, + (CheatEngineToolNames.MonoGetStatus, null), + (CheatEngineToolNames.MonoListAssemblies, "[1] = 0, [2] = 1"), + (CheatEngineToolNames.MonoListClasses, "[1] = \"6\", [2] = 0, [3] = 1, [4] = \"Player\""), + (CheatEngineToolNames.MonoListFields, "[1] = \"9\", [2] = false, [3] = 1, [4] = \"health\""), + (CheatEngineToolNames.MonoListMethods, "[1] = \"9\", [2] = 0, [3] = 1, [4] = \"Damage\"")); + } + + [Fact] + public void MonoLookupAdapters_MapClassMethodAndStaticAddressWithExactHandles() + { + StateTestHarness harness = new(); + harness.Answer(static _ => new MonoClass("9", "Player", "Game")); + harness.Answer(static _ => new MonoMethod("11", "Damage", "System.Int32", "System.Void")); + harness.Answer(static _ => new MonoStaticFieldAddress("9", "3", "7FF03000")); + MonoTools tools = Mono(harness); + + MonoClass @class = tools.FindClass(" Game ", " Player ", Token); + MonoMethod method = tools.FindMethod(" 9 ", " Damage ", Token); + MonoStaticFieldAddress address = tools.GetStaticFieldAddress(" 9 ", " 3 ", Token); + + Assert.Equal(("9", "Player", "Game"), (@class.Handle, @class.Name, @class.Namespace)); + Assert.Equal(("11", "Damage", "System.Int32", "System.Void"), (method.Handle, method.Name, method.Signature, + method.ReturnType)); + Assert.Equal(("9", "3", "7FF03000"), (address.ClassHandle, address.DomainHandle, address.Address)); + AssertOperations(harness, + (CheatEngineToolNames.MonoFindClass, "[1] = \"Game\", [2] = \"Player\""), + (CheatEngineToolNames.MonoFindMethod, "[1] = \"9\", [2] = \"Damage\""), + (CheatEngineToolNames.MonoGetStaticFieldAddress, "[1] = \"9\", [2] = \"3\"")); + } + + [Fact] + public void MonoGatedMutationAdapters_DeclareTheGateMapResultsAndReleaseAttachment() + { + StateTestHarness harness = new(); + harness.Answer(static _ => new MonoAttachResult(true, false, 7, ["collector_injected"])); + harness.Answer(static _ => new MonoCompiledMethod("11", "7FF04000")); + harness.Answer(static _ => new MonoMethodInvocation(true, "17")); + harness.Answer(static _ => new MonoDetachResult(true, [])); + MonoTools tools = Mono(harness); + + AssertTargetCodeExecutionGate(nameof(MonoTools.Attach)); + AssertTargetCodeExecutionGate(nameof(MonoTools.CompileMethod)); + AssertTargetCodeExecutionGate(nameof(MonoTools.InvokeMethod)); + MonoAttachResult attached = tools.Attach(Token); + MonoCompiledMethod compiled = tools.CompileMethod(" 11 ", Token); + MonoMethodInvocation invoked = tools.InvokeMethod(" 11 ", " 401000 ", [new MonoInvokeArgument(2, "7")], Token); + MonoDetachResult detached = tools.Detach(Token); + + Assert.Equal((true, 7L, "collector_injected"), (attached.Attached, attached.CollectorVersion, + Assert.Single(attached.HostEffects))); + Assert.Equal(("11", "7FF04000"), (compiled.MethodHandle, compiled.NativeAddress)); + Assert.Equal((true, "17", null), (invoked.Invoked, invoked.ReturnValue, invoked.Exception)); + Assert.True(detached.Detached); + Assert.Empty(harness.Resources.List()); + AssertOperations(harness, + (CheatEngineToolNames.MonoAttach, "[1] = \"mono-"), + (CheatEngineToolNames.MonoCompileMethod, "[1] = \"11\""), + (CheatEngineToolNames.MonoInvokeMethod, "[1] = \"11\", [2] = \"401000\", [3] = {{2,7,},}"), + (CheatEngineToolNames.MonoDetach, "[1] = \"mono-")); + Assert.Equal(4, harness.Dispatches); + } + + private static DotNetTools DotNet(StateTestHarness harness) => new(harness.Dispatch, harness.Jobs); + + private static MonoTools Mono(StateTestHarness harness) => new(harness.Dispatch, harness.Jobs, harness.Resources); + + private static void AssertTargetCodeExecutionGate(string methodName) + { + MethodInfo method = typeof(MonoTools).GetMethod(methodName) ?? throw new InvalidOperationException(methodName); + RequiresFeatureAttribute gate = Assert.Single(method.GetCustomAttributes()); + Assert.Equal(McpFeature.TargetCodeExecution, gate.Feature); + } + + private static void AssertOperations(StateTestHarness harness, params (string Name, string? Arguments)[] expected) + { + (string Operation, string Source)[] actual = [.. harness.LuaCalls]; + Assert.Equal(expected.Select(static item => item.Name), actual.Select(static item => item.Operation)); + for (int index = 0; index < expected.Length; index++) + { + if (expected[index].Arguments is { } arguments) + { + Assert.Contains(arguments, actual[index].Source, StringComparison.Ordinal); + } + } + } +} diff --git a/tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerScanFileTests.cs b/tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerScanFileTests.cs index b1402c6..c11b3dc 100644 --- a/tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerScanFileTests.cs +++ b/tests/CheatEngine.Mcp.Tests/Tools/Pointer/PointerScanFileTests.cs @@ -80,6 +80,24 @@ public void Load_Over32MiBStream_RejectsBeforeParsing() Assert.Throws(() => PointerScanFile.Load(oversized, CancellationToken.None)); } + [Fact] + public void Load_NonSeekableOver32MiBStream_RejectsBeforeParsingAndLeavesInputOpen() + { + byte[] content = new byte[PointerScanFile.MaximumBytes + 1]; + Array.Fill(content, (byte) ' '); + Encoding.UTF8.GetBytes(""" + {"version":2,"mapName":"m","target":0,"width":8,"paths":[],"incomplete":false} + """).CopyTo(content, 0); + using ChunkedReadStream input = new(content, 81920); + + InvalidDataException exception = Assert.Throws(() => + PointerScanFile.Load(input, CancellationToken.None)); + + Assert.Equal("Pointer scan JSON exceeds the 33554432-byte limit.", exception.Message); + Assert.Equal(PointerScanFile.MaximumBytes + 1, input.BytesRead); + Assert.False(input.WasDisposed); + } + [Fact] public void Load_JsonExceedingMaximumDepth_Rejects() { @@ -90,6 +108,110 @@ public void Load_JsonExceedingMaximumDepth_Rejects() Assert.ThrowsAny(() => PointerScanFile.Load(json, CancellationToken.None)); } + [Fact] + public void SaveLoad_Version2_ExplicitCompleteCoverage_RoundTripsWithoutChangingIncompleteState() + { + PointerScanSnapshot original = new("complete", 0x401000, 8, + [ + new PointerPath(0x400000, null, 0, [-8, 16]) + ], true, CoverageIncomplete: false); + using MemoryStream json = new(); + + PointerScanFile.Save(json, original, CancellationToken.None); + json.Position = 0; + PointerScanSnapshot loaded = PointerScanFile.Load(json, CancellationToken.None); + + Assert.True(loaded.Incomplete); + Assert.False(loaded.CoverageIncomplete); + Assert.Equal([-8L, 16], Assert.Single(loaded.Paths).Offsets); + } + + [Fact] + public void Save_InvalidMapName_FailsBeforeWritingAnyJson() + { + PointerScanSnapshot invalid = new(" ", 0, 8, + [ + new PointerPath(0x400000, null, 0, [0]) + ], false); + using MemoryStream output = new(); + + ArgumentException exception = Assert.Throws(() => + PointerScanFile.Save(output, invalid, CancellationToken.None)); + + Assert.Equal("snapshot", exception.ParamName); + Assert.Empty(output.ToArray()); + } + + [Theory] + [MemberData(nameof(InvalidSnapshotCaseIds))] + public void Validate_InvalidSnapshot_RejectsTheSpecificInvariant(string caseId) + { + (PointerScanSnapshot snapshot, string expectedMessage) = CreateInvalidSnapshot(caseId); + ArgumentException exception = Assert.Throws(() => PointerScanFile.Validate(snapshot)); + + Assert.Equal("snapshot", exception.ParamName); + Assert.Contains(expectedMessage, exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void Load_NonSeekablePartialReads_ConsumesAllChunksAndPreservesCallerStreamOwnership() + { + byte[] content = Encoding.UTF8.GetBytes( + "{\"version\":2,\"mapName\":\"chunked\",\"target\":4096,\"width\":8,\"paths\":[],\"incomplete\":false}"); + using ChunkedReadStream input = new(content, chunkSize: 3); + + PointerScanSnapshot loaded = PointerScanFile.Load(input, CancellationToken.None); + + Assert.Equal(("chunked", 4096UL, 8), (loaded.MapName, loaded.Target, loaded.Width)); + Assert.False(input.WasDisposed); + Assert.Equal(content.Length, input.BytesRead); + } + + [Fact] + public void Load_PartialReadFailure_LeavesCallerOwnedInputOpen() + { + byte[] content = Encoding.UTF8.GetBytes("{\"version\":2"); + using ChunkedReadStream input = new(content, chunkSize: 4, failAfterFirstRead: true); + + Assert.Throws(() => PointerScanFile.Load(input, CancellationToken.None)); + + Assert.False(input.WasDisposed); + Assert.Equal(4, input.BytesRead); + } + + [Theory] + [MemberData(nameof(MalformedSchemaCaseIds))] + public void Load_MalformedOrPartialSchema_Rejects(string caseId) + { + using MemoryStream json = Json(CreateMalformedSchemaContent(caseId)); + + Assert.Throws(() => PointerScanFile.Load(json, CancellationToken.None)); + } + + [Fact] + public void Load_WriteOnlyStream_RejectsBeforeAttemptingToRead() + { + using WriteOnlyStream input = new(); + + ArgumentException exception = Assert.Throws(() => + PointerScanFile.Load(input, CancellationToken.None)); + + Assert.Equal("input", exception.ParamName); + Assert.Equal(0, input.ReadAttempts); + } + + [Fact] + public void Save_ReadOnlyStream_RejectsBeforeAttemptingToWrite() + { + PointerScanSnapshot snapshot = new("map", 0, 8, [new PointerPath(0, null, 0, [0])], false); + using MemoryStream output = new(Array.Empty(), writable: false); + + ArgumentException exception = Assert.Throws(() => + PointerScanFile.Save(output, snapshot, CancellationToken.None)); + + Assert.Equal("output", exception.ParamName); + } + [Fact] public void SnapshotForSave_DeepCopiesPathsAndRefusesConcurrentRescan() { @@ -127,6 +249,45 @@ public void SnapshotForSave_DeepCopiesPathsAndRefusesConcurrentRescan() private static MemoryStream Json(string content) => new(Encoding.UTF8.GetBytes(content)); + public static IEnumerable InvalidSnapshotCaseIds => + [ + ["invalid_width"], + ["contradictory_coverage"], + ["narrow_target_overflow"], + ["absolute_path_offset"] + ]; + + private static (PointerScanSnapshot Snapshot, string ExpectedMessage) CreateInvalidSnapshot(string caseId) => caseId switch + { + "invalid_width" => + (new PointerScanSnapshot("map", 0, 3, [new PointerPath(0, null, 0, [0])], false), "width must be 4 or 8"), + "contradictory_coverage" => + (new PointerScanSnapshot("map", 0, 8, [new PointerPath(0, null, 0, [0])], false, CoverageIncomplete: true), + "incomplete source"), + "narrow_target_overflow" => + (new PointerScanSnapshot("map", uint.MaxValue + 1UL, 4, [new PointerPath(0, null, 0, [0])], false), "target does not fit"), + "absolute_path_offset" => + (new PointerScanSnapshot("map", 0, 8, [new PointerPath(0, null, 1, [0])], false), "Absolute pointer paths"), + _ => throw new ArgumentOutOfRangeException(nameof(caseId), caseId, "Unknown invalid snapshot case.") + }; + + public static IEnumerable MalformedSchemaCaseIds => + [ + ["empty_file"], + ["missing_width"], + ["paths_not_array"], + ["offsets_not_array"] + ]; + + private static string CreateMalformedSchemaContent(string caseId) => caseId switch + { + "empty_file" => "", + "missing_width" => "{\"version\":2,\"mapName\":\"m\",\"target\":0,\"paths\":[],\"incomplete\":false}", + "paths_not_array" => "{\"version\":2,\"mapName\":\"m\",\"target\":0,\"width\":8,\"paths\":{},\"incomplete\":false}", + "offsets_not_array" => "{\"version\":2,\"mapName\":\"m\",\"target\":0,\"width\":8,\"paths\":[{\"baseAddress\":0,\"module\":null,\"moduleOffset\":0,\"offsets\":0}],\"incomplete\":false}", + _ => throw new ArgumentOutOfRangeException(nameof(caseId), caseId, "Unknown malformed schema case.") + }; + private sealed class DeclaredLengthStream(long length) : Stream { public override bool CanRead => true; @@ -149,4 +310,87 @@ public override void Flush() public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } + + private sealed class ChunkedReadStream(byte[] content, int chunkSize, bool failAfterFirstRead = false) : Stream + { + private readonly byte[] _content = content; + private readonly int _chunkSize = chunkSize; + private readonly bool _failAfterFirstRead = failAfterFirstRead; + private int _position; + private int _readCount; + + internal bool WasDisposed + { + get; private set; + } + internal int BytesRead => _position; + public override bool CanRead => !WasDisposed; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => _position; + set => throw new NotSupportedException(); + } + + public override void Flush() => throw new NotSupportedException(); + public override int Read(byte[] buffer, int offset, int count) + { + if (_failAfterFirstRead && _readCount > 0) + { + throw new IOException("Synthetic partial-read failure."); + } + + int remaining = _content.Length - _position; + if (remaining == 0) + { + return 0; + } + + int read = Math.Min(Math.Min(remaining, count), _chunkSize); + Array.Copy(_content, _position, buffer, offset, read); + _position += read; + _readCount++; + return read; + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + protected override void Dispose(bool disposing) + { + WasDisposed = true; + base.Dispose(disposing); + } + } + + private sealed class WriteOnlyStream : Stream + { + internal int ReadAttempts + { + get; private set; + } + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => 0; + public override long Position + { + get; set; + } + public override void Flush() + { + } + public override int Read(byte[] buffer, int offset, int count) + { + ReadAttempts++; + throw new NotSupportedException(); + } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) + { + } + } } diff --git a/tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs b/tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs index 89d825d..0680c3c 100644 --- a/tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs +++ b/tests/CheatEngine.Mcp.Tests/Tools/Record/RecordToolsTests.cs @@ -161,6 +161,119 @@ public void ListAndFind_DescribeTheWholeListWithNestedRecords() StringComparison.Ordinal); } + [Fact] + public void GetSelected_NoSelection_ReturnsTheNormalUnselectedResult() + { + RecordTarget target = new(); + + RecordSelection selection = new RecordReadTools(target.Dispatch).GetSelected(Token); + + Assert.Equal(new RecordSelection(false, null), selection); + Assert.Equal(["Tables.TryGetSelectedRecord"], target.Calls); + } + + [Fact] + public void GetSelected_KnownRecord_MapsTheCurrentClientSnapshot() + { + RecordTarget target = new() + { + SelectedId = 2 + }; + target.Records.AddRange([Snapshot(1, 0, "Health"), Snapshot(2, 1, "Ammo", "game.exe+24", "30")]); + + RecordSelection selection = new RecordReadTools(target.Dispatch).GetSelected(Token); + + Assert.True(selection.Selected); + RecordEntry record = Assert.IsType(selection.Record); + Assert.Equal((2, "Ammo", "game.exe+24", "30"), + (record.Id, record.Description, record.Address, record.Value)); + Assert.Equal(["Tables.TryGetSelectedRecord"], target.Calls); + } + + [Fact] + public void Select_KnownRecord_ForwardsItsIdAndReturnsTheMappedSnapshot() + { + RecordTarget target = new(); + target.Records.Add(Snapshot(7, 0, "Health", "game.exe+10", "99")); + + RecordEntry selected = new RecordMutationTools(target.Dispatch).Select(7, Token); + + Assert.Equal((7, "Health", "game.exe+10", "99"), + (selected.Id, selected.Description, selected.Address, selected.Value)); + Assert.Equal(7, target.SelectedId); + Assert.Equal(["Tables.SelectRecord"], target.Calls); + } + + [Fact] + public void Move_ParentAndRoot_ForwardTheExactParentRequestsAndReturnMappedRecords() + { + RecordTarget target = new(); + target.Records.AddRange([Snapshot(2, 0, "Ammo"), Snapshot(7, 1, "Player", variableType: VariableType.Grouped)]); + RecordMutationTools tools = new(target.Dispatch); + + RecordMoveResult nested = tools.Move(2, 7, Token); + RecordMoveResult root = tools.Move(2, cancellationToken: Token); + + Assert.Equal((2, "Ammo"), (nested.Record.Id, nested.Record.Description)); + Assert.Equal((2, "Ammo"), (root.Record.Id, root.Record.Description)); + Assert.Equal([(2, 7), (2, (int?) null)], target.Parents); + Assert.Equal(["Tables.SetParent", "Tables.SetParent"], target.Calls); + } + + [Fact] + public void Move_SelfParent_RefusesBeforeAnyClientMutation() + { + RecordTarget target = new(); + + CheatEngineToolException exception = Assert.Throws(() => + new RecordMutationTools(target.Dispatch).Move(5, 5, Token)); + + Assert.Equal((ToolErrorKind.InvalidArgument, ToolHostEffect.NotStarted, "parentId"), + (exception.Error.Kind, exception.Error.HostEffect, Parameter(exception))); + Assert.Empty(target.Calls); + Assert.Empty(target.Parents); + } + + [Fact] + public void Group_ParentedMembers_PreservesRequestedOrderAndRecordsTheClientEffects() + { + RecordTarget target = new(); + target.Records.AddRange( + [ + Snapshot(1, 0, "Health"), Snapshot(2, 1, "Ammo"), + Snapshot(3, 2, "Player", variableType: VariableType.Grouped) + ]); + + RecordGroupResult result = new RecordMutationTools(target.Dispatch).Group("Stats", [2, 1], 3, Token); + + Assert.Equal((4, "Stats"), (result.Group.Id, result.Group.Description)); + Assert.Equal([2, 1], result.Records.Select(static record => record.Id)); + Assert.Equal(new MemoryRecordDefinition("Stats", "0", string.Empty, VariableType.Grouped, + new MemoryRecordId(3)), Assert.Single(target.Definitions)); + Assert.Equal([(2, 4), (1, 4)], target.Parents); + Assert.Equal(["Tables.Create", "Tables.SetParent", "Tables.SetParent", "Tables.GetRecord"], target.Calls); + } + + [Fact] + public void Group_LaterParentRefusal_LeavesTheGroupAndEarlierMoveInPlace() + { + RecordTarget target = new() + { + RefusedParentId = 2 + }; + target.Records.AddRange([Snapshot(1, 0, "Health"), Snapshot(2, 1, "Ammo")]); + + CheatEngineToolException exception = Assert.Throws(() => + new RecordMutationTools(target.Dispatch).Group("Stats", [1, 2], cancellationToken: Token)); + + Assert.Equal((ToolErrorKind.HostRefused, ToolHostEffect.Started), + (exception.Error.Kind, exception.Error.HostEffect)); + Assert.Equal(new MemoryRecordDefinition("Stats", "0", string.Empty, VariableType.Grouped, null), + Assert.Single(target.Definitions)); + Assert.Equal([(1, 3)], target.Parents); + Assert.Equal(["Tables.Create", "Tables.SetParent", "Tables.SetParent"], target.Calls); + } + [Fact] public void Create_BatchContainingAutoAssemblerWithoutGate_RefusesBeforeCreatingAnyRecord() { @@ -1609,6 +1722,20 @@ internal List Searches get; } = []; + /// The current selected record, or null when Cheat Engine has no address-list selection. + internal int? SelectedId + { + get; + set; + } + + /// The child for which the typed Client refuses a parent change after earlier mutations. + internal int? RefusedParentId + { + get; + init; + } + /// Ids the Client reports as handed out before a later table load. internal HashSet StaleIds { @@ -1659,6 +1786,8 @@ internal MemoryRecordHierarchyRequest HierarchyRequest nameof(ITableClient.GetRecordAt) => Records[(int) values[0]!], nameof(ITableClient.GetRecord) => Get(new MemoryRecordId(((MemoryRecordId) values[0]!).Value)), nameof(ITableClient.TryGetRecord) => TryGet(values), + nameof(ITableClient.TryGetSelectedRecord) => TryGetSelected(values), + nameof(ITableClient.SelectRecord) => Select(new MemoryRecordId(((MemoryRecordId) values[0]!).Value)), nameof(ITableClient.GetHierarchy) => Tree((MemoryRecordId) values[0]!, (MemoryRecordHierarchyRequest) values[1]!), nameof(ITableClient.Create) => Add((MemoryRecordDefinition) values[0]!), @@ -1754,6 +1883,26 @@ private bool TryGet(object?[] values) return true; } + private bool TryGetSelected(object?[] values) + { + if (SelectedId is not { } id) + { + values[0] = default(MemoryRecordSnapshot); + values[1] = default(CheatEngineFailure); + return false; + } + + values[0] = Get(new MemoryRecordId(id)); + values[1] = default(CheatEngineFailure); + return true; + } + + private MemoryRecordSnapshot Select(MemoryRecordId id) + { + SelectedId = id.Value; + return Get(id); + } + private MemoryRecordSnapshot Add(MemoryRecordDefinition definition) { Definitions.Add(definition); @@ -1808,6 +1957,12 @@ private MemoryRecordSnapshot Update(MemoryRecordId id, MemoryRecordUpdate update private MemoryRecordSnapshot Parent(MemoryRecordId child, MemoryRecordId? parent) { + if (child.Value == RefusedParentId) + { + throw new CheatEngineFailure(CheatEngineFailureKind.OperationRejected, "Tables.SetParent", + "Cheat Engine refused the parent change.", hostEffect: CheatEngineHostEffect.Started).ToException(); + } + Parents.Add((child.Value, parent?.Value)); return Get(child); } diff --git a/tests/CheatEngine.Mcp.Tests/Tools/Util/UtilToolsTests.cs b/tests/CheatEngine.Mcp.Tests/Tools/Util/UtilToolsTests.cs index c928ba1..692800a 100644 --- a/tests/CheatEngine.Mcp.Tests/Tools/Util/UtilToolsTests.cs +++ b/tests/CheatEngine.Mcp.Tests/Tools/Util/UtilToolsTests.cs @@ -26,6 +26,37 @@ public void ConvertValue_RawBytes_DecodesTheTargetBitPattern() Assert.Equal(("FE FF", "-2"), (result.Bytes, result.Value)); } + [Theory] + [MemberData(nameof(FixedScalarDecodingCases))] + public void ConvertValue_RawBytes_DecodesEachFixedScalarType(string bytes, McpValueType targetType, + int pointerSize, string expected) + { + UtilConvertValueResult result = UtilTools.ConvertValue(bytes, McpValueType.Bytes, targetType, + ValueByteOrder.LittleEndian, pointerSize); + + Assert.Equal((bytes, expected), (result.Bytes, result.Value)); + } + + [Fact] + public void ConvertValue_BigEndianRawBytes_UsesTheRequestedPresentationOrder() + { + UtilConvertValueResult result = UtilTools.ConvertValue("00 80", McpValueType.Bytes, McpValueType.Int16, + ValueByteOrder.BigEndian); + + Assert.Equal(("80 00", "-32768"), (result.Bytes, result.Value)); + } + + [Theory] + [MemberData(nameof(TextConversionCases))] + public void ConvertValue_TextEncoding_PreservesItsDefinedByteOrder(McpValueType sourceType, string value, + McpValueType targetType, ValueByteOrder byteOrder, string expectedBytes, string expectedValue) + { + UtilConvertValueResult result = UtilTools.ConvertValue(value, sourceType, targetType, + byteOrder); + + Assert.Equal((expectedBytes, expectedValue), (result.Bytes, result.Value)); + } + [Fact] public void ConvertValue_MismatchedScalarWidths_IsInvalidArgument() { @@ -36,11 +67,36 @@ public void ConvertValue_MismatchedScalarWidths_IsInvalidArgument() Assert.StartsWith("targetType", exception.Error.Message, StringComparison.Ordinal); } + [Theory] + [InlineData(0)] + [InlineData(16)] + public void ConvertValue_InvalidPointerWidth_IsRejectedBeforeConversion(int pointerSize) + { + CheatEngineToolException exception = Assert.Throws(() => + UtilTools.ConvertValue("1", McpValueType.UInt8, McpValueType.UInt8, pointerSize: pointerSize)); + + Assert.Equal(ToolErrorKind.InvalidArgument, exception.Error.Kind); + Assert.Equal("""{"parameter":"pointerSize"}""", exception.Error.Details!.Value.GetRawText()); + } + + [Fact] + public void ConvertValue_OddUtf16ByteCount_IsReportedAsATargetTypeError() + { + CheatEngineToolException exception = Assert.Throws(() => + UtilTools.ConvertValue("41", McpValueType.Bytes, McpValueType.WString)); + + Assert.Equal(ToolErrorKind.InvalidArgument, exception.Error.Kind); + Assert.Equal("""{"parameter":"targetType"}""", exception.Error.Details!.Value.GetRawText()); + Assert.Contains("even number of bytes", exception.Error.Message, StringComparison.Ordinal); + } + [Theory] [InlineData("0xFFFFFFFFFFFFFFFF + 2", "1", "1", "1")] [InlineData("0x8000000000000000 >> 1", "4611686018427387904", "4611686018427387904", "4000000000000000")] [InlineData("(1 << 63) + (1 << 63)", "0", "0", "0")] [InlineData("~0", "18446744073709551615", "-1", "FFFFFFFFFFFFFFFF")] + [InlineData("7 * 6", "42", "42", "2A")] + [InlineData("7 - 9", "18446744073709551614", "-2", "FFFFFFFFFFFFFFFE")] public void Calculate_UsesUnsigned64BitWraparound(string expression, string unsignedValue, string signedValue, string hex) { @@ -49,6 +105,15 @@ public void Calculate_UsesUnsigned64BitWraparound(string expression, string unsi Assert.Equal((unsignedValue, signedValue, hex), (result.UnsignedValue, result.SignedValue, result.Hex)); } + [Theory] + [MemberData(nameof(OperatorPrecedenceCases))] + public void Calculate_UsesDocumentedOperatorPrecedenceAndShiftMasking(string expression, string expected) + { + UtilCalculateResult result = UtilTools.Calculate(expression); + + Assert.Equal(expected, result.UnsignedValue); + } + [Theory] [InlineData("1 / 0")] [InlineData("0x")] @@ -61,4 +126,83 @@ public void Calculate_InvalidExpression_IsInvalidArgument(string expression) Assert.Equal(ToolErrorKind.InvalidArgument, exception.Error.Kind); Assert.StartsWith("expression", exception.Error.Message, StringComparison.Ordinal); } + + [Theory] + [MemberData(nameof(InvalidExpressionCases))] + public void Calculate_InvalidExpression_ReportsTheSpecificRejectedConstruct(string expression, string detail) + { + CheatEngineToolException exception = Assert.Throws(() => UtilTools.Calculate(expression)); + + Assert.Equal(ToolErrorKind.InvalidArgument, exception.Error.Kind); + Assert.Equal("""{"parameter":"expression"}""", exception.Error.Details!.Value.GetRawText()); + Assert.Contains(detail, exception.Error.Message, StringComparison.Ordinal); + } + + [Fact] + public void Calculate_ExcessiveInputAndNesting_AreRefusedWithTheirOwnErrors() + { + CheatEngineToolException length = Assert.Throws(() => + UtilTools.Calculate(new string('0', 4097))); + CheatEngineToolException depth = Assert.Throws(() => + UtilTools.Calculate(new string('(', 65) + "1" + new string(')', 65))); + + Assert.Equal(ToolErrorKind.LimitExceeded, length.Error.Kind); + Assert.Contains("at most 4096", length.Error.Message, StringComparison.Ordinal); + Assert.Equal(ToolErrorKind.InvalidArgument, depth.Error.Kind); + Assert.Contains("more than 64 nested parentheses", depth.Error.Message, StringComparison.Ordinal); + } + + public static TheoryData FixedScalarDecodingCases() + { + return new TheoryData + { + { "80", McpValueType.Int8, 8, "-128" }, + { "FF", McpValueType.UInt8, 8, "255" }, + { "CD AB", McpValueType.UInt16, 8, "43981" }, + { "00 00 00 80", McpValueType.Int32, 8, "-2147483648" }, + { "78 56 34 12", McpValueType.UInt32, 8, "305419896" }, + { "FF FF FF FF FF FF FF 7F", McpValueType.Int64, 8, "9223372036854775807" }, + { "FF FF FF FF FF FF FF FF", McpValueType.UInt64, 8, "18446744073709551615" }, + { "00 00 A0 3F", McpValueType.Float, 8, "1.25" }, + { "00 00 00 00 00 00 F0 3F", McpValueType.Double, 8, "1" }, + { "78 56 34 12", McpValueType.Pointer, 4, "12345678" }, + { "D0 C3 B2 A1 F6 7F 00 00", McpValueType.Pointer, 8, "7FF6A1B2C3D0" } + }; + } + + public static TheoryData TextConversionCases() + { + return new TheoryData + { + { McpValueType.String, "hé", McpValueType.Bytes, ValueByteOrder.BigEndian, "68 C3 A9", "68 C3 A9" }, + { McpValueType.Bytes, "68 C3 A9", McpValueType.String, ValueByteOrder.LittleEndian, "68 C3 A9", "hé" }, + { McpValueType.WString, "hé", McpValueType.Bytes, ValueByteOrder.BigEndian, "68 00 E9 00", "68 00 E9 00" }, + { McpValueType.Bytes, "68 00 E9 00", McpValueType.WString, ValueByteOrder.LittleEndian, "68 00 E9 00", "hé" } + }; + } + + public static TheoryData OperatorPrecedenceCases() + { + return new TheoryData + { + { "1 | 2 & 4", "1" }, + { "1 ^ 3 & 1", "0" }, + { "1 + 2 << 3", "24" }, + { "20 / 3 % 4", "2" }, + { "-1 >> 63", "1" }, + { "1 << 64", "1" } + }; + } + + public static TheoryData InvalidExpressionCases() + { + return new TheoryData + { + { " ", "must contain an integer expression" }, + { "(1", "unclosed parenthesis" }, + { "1 # 2", "unsupported token" }, + { "1 % 0", "cannot take a remainder by zero" }, + { "18446744073709551616", "invalid 64-bit decimal literal" } + }; + } } diff --git a/tests/CheatEngine.Mcp.Tests/packages.lock.json b/tests/CheatEngine.Mcp.Tests/packages.lock.json index 9107079..e21c342 100644 --- a/tests/CheatEngine.Mcp.Tests/packages.lock.json +++ b/tests/CheatEngine.Mcp.Tests/packages.lock.json @@ -66,28 +66,28 @@ }, "CheatEngine.Client.Abstractions": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "1Ma1AEwpAZ2Q6c16ydNzOIsnchanixkH0VgSASOoRoeV+DPtTp98zfi4ZDLat/fkMXxPJNysCGRVRxm5hES+eg==", + "resolved": "1.1.0", + "contentHash": "CpaOmRG1KJ7f6QxWXowEp8ySvBbb7qTQWB12ulocau35SUYpz9Ug0R52zYoJc+nEeVcQhLCGp/nzuoIJHD9tfg==", "dependencies": { "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Core": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "MHYLqRPy1NGm8k5uyMB9PnazHwPCyQgs1qUYrei47M0gDUW3yChZJ6N7xrcsng4bJTiqUqIvfOIoPU3MCSr5VA==", + "resolved": "1.1.0", + "contentHash": "p3Av7agZlLZxr5fvwbW2Uq8TPwOT+PTzs/Hs92zcA8egpQUqyhnAzQOSWPJUNVk+TYDnXWj0nvQwcDMiaEfkTw==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)" } }, "CheatEngine.Client.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "d7zKtwOIkBg7dyXLMP8Z9saHVRj1D82/wg4mAo6ds37ecVylVWzsGusNU2HXiN705pTbs6NRB7k8LEak/KR3Cw==", + "resolved": "1.1.0", + "contentHash": "ARBq9El8zUijcol/NS/mabxeWHVhPc8flDWQLnEIYoFo9nipLgPwG8SzZT+IckA0BqPtu6LPn3UZDsHReBYqSA==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]", - "CheatEngine.Client.Core": "[1.0.0]", + "CheatEngine.Client.Abstractions": "[1.1.0]", + "CheatEngine.Client.Core": "[1.1.0]", "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", "Microsoft.Extensions.DependencyInjection": "10.0.12", "Microsoft.Extensions.Logging": "10.0.12", @@ -97,18 +97,18 @@ }, "CheatEngine.Client.Fluent": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "8/4JcuUjaG7iWHgpg6j+nt3r4xSfV5KyjlJ9NlMbX0SWKoRIbTXeUezQjmA0X5XfC76JOmWLBCPuPGbKHv4dmg==", + "resolved": "1.1.0", + "contentHash": "Q4baX9mQpyuGRO8x1ROOMidejY1o6I9Zkh2uCFEHMIkZK+bdzMas+yQzPyJt80jQUH0InAqCb1tnk4AjELRQZQ==", "dependencies": { - "CheatEngine.Client.Abstractions": "[1.0.0]" + "CheatEngine.Client.Abstractions": "[1.1.0]" } }, "CheatEngine.Client.Hosting": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "NxpEXtZ6VN1URhR3mrD6uAqr7Os5zp2y7k45X1+2k0M68XBQC+HIobp9QUBb/q5d2tDgAlZxpHWuS6xhasqJTQ==", + "resolved": "1.1.0", + "contentHash": "atDiA6dpnP7Mo6dZ9G3XIou8rc2Vc9g5YKvYnps7hwzKP+pY8NUC6O3J8/IiyvN8G0QIZeL7QGSILt8AuCnqIg==", "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[1.0.0]", + "CheatEngine.Client.Extensions.DependencyInjection": "[1.1.0]", "CheatEngine.SDK": "[2.0.0, 3.0.0)", "Microsoft.Extensions.DependencyInjection": "10.0.12", "Microsoft.Extensions.Logging": "10.0.12" @@ -339,74 +339,74 @@ "CheatEngine.Mcp": { "type": "Project", "dependencies": { - "CheatEngine.Client": "[1.0.0, )", - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Hosting": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Prompts": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Resources": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Tools": "[2.0.0-beta.3, )", - "CheatEngine.SDK": "[2.0.0, )", + "CheatEngine.Client": "[1.1.0, )", + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Hosting": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Prompts": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Resources": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Tools": "[2.0.0-beta.4, )", + "CheatEngine.SDK": "[2.0.1, )", "ModelContextProtocol.AspNetCore": "[2.2.0, )" } }, "cheatengine.mcp.core": { "type": "Project", "dependencies": { - "CheatEngine.Client": "[1.0.0, )", + "CheatEngine.Client": "[1.1.0, )", "ModelContextProtocol": "[2.2.0, )" } }, "cheatengine.mcp.gateway": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Hosting": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Prompts": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Resources": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Tools": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Hosting": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Prompts": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Resources": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Tools": "[2.0.0-beta.4, )" } }, "cheatengine.mcp.hosting": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", "ModelContextProtocol.AspNetCore": "[2.2.0, )" } }, "cheatengine.mcp.prompts": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Resources": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Resources": "[2.0.0-beta.4, )" } }, "cheatengine.mcp.resources": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )", - "CheatEngine.Mcp.Tools": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )", + "CheatEngine.Mcp.Tools": "[2.0.0-beta.4, )" } }, "cheatengine.mcp.tools": { "type": "Project", "dependencies": { - "CheatEngine.Mcp.Core": "[2.0.0-beta.3, )" + "CheatEngine.Mcp.Core": "[2.0.0-beta.4, )" } }, "CheatEngine.Client": { "type": "CentralTransitive", - "requested": "[1.0.0, )", - "resolved": "1.0.0", - "contentHash": "nJ262v9o4Z2QXmG8+Ny5G0V850IYQIdL6GMnugvwPQiqyI3zZgOplU0HBSM+vX1B8k+geTXUFS53IZA03EtigA==", + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "7Gwflxn1fzSMgHFecIhnUwiOHHuM2ysBKa8e0okkr6S/b8Xv821vlGpJHA0LGT8rzhJ8Otbx5PH03//pfgrNVw==", "dependencies": { - "CheatEngine.Client.Fluent": "[1.0.0]", - "CheatEngine.Client.Hosting": "[1.0.0]" + "CheatEngine.Client.Fluent": "[1.1.0]", + "CheatEngine.Client.Hosting": "[1.1.0]" } }, "CheatEngine.SDK": { "type": "CentralTransitive", - "requested": "[2.0.0, )", - "resolved": "2.0.0", - "contentHash": "NLEdZYJ9LKW3EFNB4X5snKCQf7ZS86GkCQ+El7o+S1XQcxHQGjS45Q1ap8lfjQuIwm004mQ3TPxo+ph1yvRrlQ==" + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "coRWNRz81E5N3PQeruDzHEDSeQQMSYkuCIx9LpKWO3qP61v6hbyRvYlHzjco2iNVykxy9z25zVbivUbxpA5dow==" }, "Microsoft.DiaSymReader": { "type": "CentralTransitive",