unity-mcp/Server/tests/integration/test_external_changes_scann...

87 lines
3.0 KiB
Python
Raw Permalink Normal View History

Async Test Infrastructure & Editor Readiness Status + new refresh_unity tool (#507) * Add editor readiness v2, refresh tool, and preflight guards * Detect external package changes and harden refresh retry * feat: add TestRunnerNoThrottle and async test running with background stall prevention - Add TestRunnerNoThrottle.cs: Sets editor to 'No Throttling' mode during test runs with SessionState persistence across domain reload - Add run_tests_async and get_test_job tools for non-blocking test execution - Add TestJobManager for async test job tracking with progress monitoring - Add ForceSynchronousImport to all AssetDatabase.Refresh() calls to prevent stalls - Mark DomainReloadResilienceTests as [Explicit] with documentation explaining the test infrastructure limitation (internal coroutine waits vs MCP socket polling) - MCP workflow is unaffected - socket messages provide external stimulus that keeps Unity responsive even when backgrounded * refactor: simplify and clean up code - Remove unused Newtonsoft.Json.Linq import from TestJobManager - Add throttling to SessionState persistence (once per second) to reduce overhead - Critical job state changes (start/finish) still persist immediately - Fix duplicate XML summary tag in DomainReloadResilienceTests * docs: add async test tools to README, document domain reload limitation - Add run_tests_async and get_test_job to main README tools list - Document background stall limitation for domain reload tests in DEV readme * ci: add separate job for domain reload tests Run [Explicit] domain_reload tests in their own job using -testCategory * ci: run domain reload tests in same job as regular tests Combines into single job with two test steps to reuse cached Library * fix: address coderabbit review issues - Fix TOCTOU race in TestJobManager.StartJob (single lock scope for check-and-set) - Store TestRunnerApi reference with HideAndDontSave to prevent GC/serialization issues * docs: update tool descriptions to prefer run_tests_async - run_tests_async is now marked as preferred for long-running suites - run_tests description notes it blocks and suggests async alternative * docs: update README screenshot to v8.6 UI * docs: add v8.6 UI screenshot * Update README for MCP version and instructions for v8.7 * fix: handle preflight busy signals and derive job status from test results - manage_asset, manage_gameobject, manage_scene now check preflight return value and propagate busy/retry signals to clients (fixes Sourcery #1) - TestJobManager.FinalizeCurrentJobFromRunFinished now sets job status to Failed when resultPayload.Failed > 0, not always Succeeded (fixes Sourcery #2) * fix: increase HTTP server startup timeout for dev mode When 'Force fresh server install' is enabled, uvx uses --no-cache --refresh which rebuilds the package and takes significantly longer to start. - Increase timeout from 10s to 45s when dev mode is enabled - Add informative log message explaining the longer startup time - Show actual timeout value in warning message * fix: derive job status from test results in FinalizeFromTask fallback Apply same logic as FinalizeCurrentJobFromRunFinished: check result.Failed > 0 to correctly mark jobs as Failed when tests fail, even in the fallback path when RunFinished callback is not delivered.
2026-01-04 04:42:32 +08:00
import os
import time
from pathlib import Path
def test_external_changes_scanner_marks_dirty_and_clears(tmp_path, monkeypatch):
# Ensure the scanner is active for this unit-style test (not gated by PYTEST_CURRENT_TEST).
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
from services.state.external_changes_scanner import ExternalChangesScanner
# Create a minimal Unity-like layout
root = tmp_path / "Project"
(root / "Assets").mkdir(parents=True)
(root / "ProjectSettings").mkdir(parents=True)
(root / "Packages").mkdir(parents=True)
inst = "Test@deadbeef"
s = ExternalChangesScanner(scan_interval_ms=0, max_entries=10000)
s.set_project_root(inst, str(root))
# Create a file before baseline so the initial scan establishes a stable reference point.
p = root / "Assets" / "x.txt"
p.write_text("hi")
# Baseline scan: should not be dirty.
first = s.update_and_get(inst)
assert first["external_changes_dirty"] is False
# Touch the file and scan again: should become dirty.
now = time.time()
os.utime(p, (now + 10.0, now + 10.0))
second = s.update_and_get(inst)
assert second["external_changes_dirty"] is True
assert isinstance(second["external_changes_last_seen_unix_ms"], int)
assert isinstance(second["dirty_since_unix_ms"], int)
# Clear and confirm dirty flag resets.
s.clear_dirty(inst)
third = s.update_and_get(inst)
assert third["external_changes_dirty"] is False
assert isinstance(third["last_cleared_unix_ms"], int)
def test_external_changes_scanner_includes_file_dependency_roots(tmp_path, monkeypatch):
# Ensure the scanner is active for this unit-style test (not gated by PYTEST_CURRENT_TEST).
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
from services.state.external_changes_scanner import ExternalChangesScanner
# Unity project root
root = tmp_path / "Project"
(root / "Assets").mkdir(parents=True)
(root / "ProjectSettings").mkdir(parents=True)
(root / "Packages").mkdir(parents=True)
# External local package root (outside project root)
pkg = tmp_path / "ExternalPkg"
(pkg / "Editor").mkdir(parents=True)
target = pkg / "Editor" / "Some.cs"
target.write_text("// v1")
# manifest.json referencing file: dependency
manifest = root / "Packages" / "manifest.json"
manifest.write_text(
'{\n "dependencies": {\n "com.example.pkg": "file:../../ExternalPkg"\n }\n}\n',
encoding="utf-8",
)
inst = "Test@deadbeef"
s = ExternalChangesScanner(scan_interval_ms=0, max_entries=10000)
s.set_project_root(inst, str(root))
# Baseline scan captures current mtimes across project + external pkg
baseline = s.update_and_get(inst)
assert baseline["external_changes_dirty"] is False
# Touch external package file and scan again -> should mark dirty
now = time.time()
os.utime(target, (now + 10.0, now + 10.0))
changed = s.update_and_get(inst)
assert changed["external_changes_dirty"] is True