| name | unity |
| description | Compile, test, and drive Unity for this repo's C# packages (unity/core, jint, quickjs, clearscript) and the two Unity projects (tests/, kitchen-sink/). Use when a change touches C# under unity/**, when Unity test results are needed, when a rendering snapshot has to be checked or regenerated, or when the app has to be started or screenshotted. Also use when asked to "run the Unity tests", "check this compiles", or "see it working in Unity". |
Driving Unity from here
Two ways in. Pick by whether an Editor is already open on the project.
| pnpm unity <cmd> (batch mode) | pnpm unity bridge <action> (open Editor) |
|---|
| Needs | the project not open in an Editor | the project open, with REACT_UNITY_DEVELOPER on the active build target |
| Speed | ~8 s compile, ~50 s EditMode suite | saves the ~8 s startup, no more |
| Use for | the default path; anything scriptable, and the only CI-faithful results | an Editor the user already has open, play mode, screenshots |
The bridge is not mainly a speed win — it is how you work without closing someone's Editor, and the only way to reach play mode and screenshots.
Both refuse to fight each other: batch mode checks the project lock first and tells you to use the bridge instead.
The loop for a C# change
pnpm unity compile tests
8 seconds warm, and it reports file(line,col): CSxxxx: message for every error. Run this after every C# edit under unity/** — it is far cheaper than the test suite and catches the majority of mistakes. Nothing else in this repo type-checks C#; pnpm typecheck is TypeScript only.
Then the suites:
pnpm unity test tests
EditMode then PlayMode (two Unity invocations — -testPlatform All is a game-ci concept that Unity rejects with exit code 4). Narrow it while iterating:
pnpm unity test tests --platform EditMode --filter ReactUnity.Tests.StyleTests
Green as of 2026-07-28 is 316/325 EditMode (9 skipped) and 655/665 PlayMode (10 skipped), both with zero failures — so a single failure is a real signal, not background noise. Zero tests is a failure, not a pass: it means the project failed to load, usually package resolution. The CLI treats it that way; do not read 0 failed as green without checking the total.
Rendering snapshots
PlayMode compares captures against unity/core/Tests/.snapshots/windows/ (committed, so local runs are meaningful). Two traps:
--nographics makes every snapshot assert go Inconclusive, not fail. Only pass it when you know you are skipping them.
- Regenerating is
--overwrite-snapshots. It rewrites committed PNGs — and only ever the windows/ set when run here, since the directory is chosen by SystemInfo.operatingSystemFamily. Only do it when asked, and show git status afterwards.
- If
TextMesh Pro Essential Resources are missing shows up, that is environmental, not yours: Unity 6's ugui 2.x does not recognise the old com.unity.textmeshpro layout, pops its importer window, and the error attaches to whichever test is mid-flight — so it lands on a different one each run. Fixed by importing TMP essentials into tests/; reimport via Window > TextMeshPro > Import TMP Essential Resources if it returns.
The React > Tests > Overwrite Snapshots menu item is a toggle on EditorPrefs, not a one-shot. If you drive it through bridge menu, you have flipped persistent state that affects every later run — flip it back.
A failing snapshot is a bug report until you have proven otherwise. On 2026-07-28 eleven gradient snapshots failed, were assumed stale, and were overwritten — enshrining a real regression (b0fc47885 had made a linear-space colour conversion unconditional, darkening every gradient by 2.2 in gamma-space projects). It had gone unnoticed for seven months because the UGUI PlayMode suite was not running on CI.
Two lessons worth more than the incident:
- Re-running after
--overwrite-snapshots proves nothing. It rewrites the baseline, so it cannot fail. The only real check is a plain run against baselines you did not generate.
- Look at the pixels.
linear-gradient(red, blue) must have centre (127,0,128) and CSS green must be (0,128,0) — the renderer mimics CSS, so a browser is the reference. Compare against git show <commit>~1:<path>; identical diff counts across Unity versions prove determinism, not correctness.
linux/ is the set CI reads and must be regenerated by CI — [snapshots] in a commit message, or the workflow's overwrite-snapshots input. Never regenerate linux/ locally.
Working against the open Editor
pnpm unity bridge status kitchen-sink
Compile errors, play state, and whether the Editor is busy. The rest: logs --level error, refresh (reimport + recompile, then report), test --platform EditMode, play / stop, screenshot --path out.png, menu --path "...".
Plus quit, which closes the Editor and waits for the lock to clear — that is how you hand a project back to batch mode: pnpm unity bridge quit tests && pnpm unity test tests.
The bridge lives in unity/core/Editor/Developer/AgentBridge on 127.0.0.1, port in <project>/Library/ReactUnityAgentBridge.json. It is compiled out without REACT_UNITY_DEVELOPER and never starts in batch mode.
An Editor that was already running before the bridge was added has no bridge until it recompiles — click into its window, or pnpm unity open a fresh one. The discovery file is the test: no file, no bridge.
Its socket goes down for the length of every domain reload, so a refused connection means "busy", not "broken" — the client already polls through it. Long operations never block a request: start them, then poll status.
Screenshots need the Editor to actually render a frame. In play mode that is automatic; idle in the Editor it may never come, and the client reports the timeout rather than a bogus success.
Batch mode is the source of truth; the bridge is for iterating. The same EditMode suite gives different answers in the two environments — measured on 6000.5.5f1, batch was 316/325 with 9 skipped and 0 failures, while the open Editor ran 3 of those skipped tests and failed 2 others (ScriptTagDoesNotCrashOnError, ActivePropertyShouldWorkForStyleTag) that batch passes. Both are sensitive to Editor state — log interception and :active — not to your change. Use the bridge for the fast loop, then confirm with pnpm unity test tests before claiming a suite is green.
Running the sample app
kitchen-sink renders a React app served by react-unity-scripts:
pnpm --filter reactunity-kitchen-sink start
pnpm unity open kitchen-sink && pnpm unity bridge play kitchen-sink
pnpm unity bridge screenshot kitchen-sink --path Logs/unity/shot.png
That produces a real PNG of the running app — read it back to check a visual change. Unity connects to the dev server, so JS changes hot-reload without touching the Editor.
Both Unity projects consume the C# packages as file:../../unity/*, so both exercise the working tree. kitchen-sink used to point at https://github.com/ReactUnity/core.git#latest — while it did, it compiled the published package and local C# changes were invisible there, silently. If a change to unity/** seems to have no effect in kitchen-sink, check its manifest first.
Those file: refs are also why kitchen-sink cannot be cloned on its own, and why publishing it is a transform rather than a copy — see prepare.mts. Anything added here that only resolves inside this checkout has to be handled there too.
Things that will bite
Local runs rewrite the project, and committing that breaks CI. Opening tests/ with 6000.5 upgrades Packages/manifest.json to a 6000-only shape (com.unity.ugui 2.x, no textmeshpro) that the CI jobs cannot resolve — measured on 6000.1, and unresolvable packages produce zero tests while still looking like a pass. The CLI snapshots those files and puts them back after every run. Do not pass --no-restore unless you intend to commit the upgrade, and check git status under tests/ before committing anything.
Restore only covers batch runs. pnpm unity open hands the project to an interactive Editor that nothing cleans up after, so it churns ProjectSettings/* and the deliberately-tracked UserSettings/EditorUserSettings.asset freely. After any session with an open Editor, read git status before committing.
Unity leaves untracked files behind (tests/.vscode/, tests/tests.slnx, new ProjectSettings/*.asset). Restore does not delete them — regenerating them each run is worse. Leave them out of commits. A killed PlayMode run also leaks tests/Assets/InitTestScene*.unity, which the test framework normally deletes itself; a stray one is debris, not content.
The editor version is pinned per project, and tests/ cannot use 6000.5. tests/ runs on 6000.1.4f1, kitchen-sink on 6000.5.5f1. This is not cosmetic: the committed tests/Packages/manifest.json resolves com.unity.inputsystem and test-framework.performance versions that still use TreeView/TreeViewItem, which 6000.5 made obsolete-as-error — 306 compile errors before a single test runs. 6000.1.4f1 compiles it clean and is the nearest install to CI's main job (6000.1.9f1). UNITY_VERSION= overrides; pnpm unity editors lists what exists. CI runs 6000.0.51f1 and 6000.1.9f1, so a local pass still is not proof the matrix passes.
Switching UNITY_VERSION is not free. A different editor deletes and recreates the project's asset database, so the run after a version switch pays a full reimport, and switching back pays it again. Worth it to reproduce a matrix failure; not worth it casually. ProjectVersion.txt is deliberately left at whatever version last opened the project — reverting it below the local editor makes pnpm unity open hang on a modal "Project Upgrade Required" dialog with no visible window title. CI ignores that file entirely.
Generated TypeScript models are not editable. packages/renderer/src/models/generated/*.ts comes from unity/core/Editor/Developer/TypescriptModelsGenerator.cs. Change the C# type and regenerate from the Editor; never hand-edit the output.
Logs and results land in Logs/unity/ (gitignored) at stable paths, so a failed run can be re-read without re-running it.