windows-development-sandbox 1.25.1 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/config.example.json +12 -3
  2. package/dist/app/features/accounts/services.def.d.ts +1 -1
  3. package/dist/app/features/accounts/services.def.js +1 -1
  4. package/dist/app/features/defs.d.ts +198 -3
  5. package/dist/app/features/defs.js +8 -0
  6. package/dist/app/features/settings/categories.def.js +1 -0
  7. package/dist/app/features/unity/index.d.ts +2 -0
  8. package/dist/app/features/unity/index.js +3 -0
  9. package/dist/app/features/unity/unity.def.d.ts +294 -0
  10. package/dist/app/features/unity/unity.def.js +471 -0
  11. package/dist/app/features/unity/unity.run.d.ts +142 -0
  12. package/dist/app/features/unity/unity.run.js +363 -0
  13. package/dist/app/registry.js +6 -0
  14. package/dist/bin/agent-sandbox.js +2 -2
  15. package/dist/bin/{chunk-F4PVBN23.js → chunk-6WWMBCT2.js} +3580 -1962
  16. package/dist/bin/{chunk-3GAXLATK.js → chunk-JXL6ZOO3.js} +72 -5
  17. package/dist/bin/{chunk-G3TG4ZT7.js → chunk-ZXCAVVS4.js} +1 -1
  18. package/dist/bin/claude-host.js +2 -2
  19. package/dist/bin/claude-sbx.js +2 -2
  20. package/dist/bin/codex-sbx.js +2 -2
  21. package/dist/bin/gemini-sbx.js +2 -2
  22. package/dist/bin/sandbox.js +2 -2
  23. package/dist/bin/sbx-hide.js +2 -2
  24. package/dist/bin/sbx-show.js +2 -2
  25. package/dist/cli/commands/launch/launch.js +12 -0
  26. package/dist/cli/commands/registry.js +3 -0
  27. package/dist/cli/manifest.d.ts +33 -1
  28. package/dist/cli/manifest.js +32 -1
  29. package/dist/cli/menu/coverage-exempt.js +1 -0
  30. package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
  31. package/dist/cli/menu/pages/sandbox-page.js +40 -3
  32. package/dist/cli/menu/views.js +2 -0
  33. package/dist/core/config/defaults.js +16 -3
  34. package/dist/core/schema/accounts.d.ts +3 -0
  35. package/dist/core/schema/accounts.js +1 -0
  36. package/dist/core/schema/config.d.ts +29 -13
  37. package/dist/core/schema/github.d.ts +1 -0
  38. package/dist/core/schema/github.js +3 -0
  39. package/dist/core/schema/pc-installs.d.ts +2 -1
  40. package/dist/core/schema/pc-installs.js +6 -1
  41. package/dist/core/schema/toolchains.d.ts +8 -0
  42. package/dist/core/schema/toolchains.js +19 -4
  43. package/dist/guest/accounts/adapters.js +2 -0
  44. package/dist/guest/accounts/apply.d.ts +4 -2
  45. package/dist/guest/accounts/kinds/perplexityapp.d.ts +7 -0
  46. package/dist/guest/accounts/kinds/perplexityapp.js +53 -0
  47. package/dist/guest/accounts/kinds/unity.d.ts +27 -0
  48. package/dist/guest/accounts/kinds/unity.js +164 -7
  49. package/dist/guest/apps/app-installer.js +18 -0
  50. package/dist/guest/apps/store-app.d.ts +4 -0
  51. package/dist/guest/apps/store-app.js +19 -0
  52. package/dist/guest/apps/store-check.d.ts +8 -0
  53. package/dist/guest/apps/store-check.js +33 -0
  54. package/dist/guest/boot/boot.js +1 -1
  55. package/dist/guest/boot/guide.js +18 -0
  56. package/dist/guest/browser/comet.d.ts +3 -1
  57. package/dist/guest/browser/comet.js +1 -1
  58. package/dist/guest/browser/perplexity-app.d.ts +12 -0
  59. package/dist/guest/browser/perplexity-app.js +60 -0
  60. package/dist/guest/daemon/daemon.js +2 -0
  61. package/dist/guest/devtools/devtool.d.ts +1 -1
  62. package/dist/guest/devtools/registry.js +1 -2
  63. package/dist/guest/entries/tool.js +1 -1
  64. package/dist/guest/setup/addon-mcp.d.ts +7 -2
  65. package/dist/guest/setup/addon-mcp.js +19 -9
  66. package/dist/guest/setup/github-token.js +4 -4
  67. package/dist/guest/setup/unity-agent.d.ts +11 -0
  68. package/dist/guest/setup/unity-agent.js +36 -0
  69. package/dist/guest/toolchains/engines/unity-cli.d.ts +24 -0
  70. package/dist/guest/toolchains/engines/unity-cli.js +74 -0
  71. package/dist/guest/toolchains/engines/unity-hub.d.ts +2 -0
  72. package/dist/guest/toolchains/engines/unity-hub.js +27 -0
  73. package/dist/guest/toolchains/engines/unity-release.d.ts +4 -15
  74. package/dist/guest/toolchains/engines/unity-release.js +17 -5
  75. package/dist/guest/toolchains/engines/unity-status.d.ts +11 -0
  76. package/dist/guest/toolchains/engines/unity-status.js +39 -0
  77. package/dist/guest/toolchains/engines/unity.d.ts +20 -1
  78. package/dist/guest/toolchains/engines/unity.js +250 -3
  79. package/dist/guest/toolchains/plan.js +5 -1
  80. package/dist/guest/toolchains/tools/cli.js +2 -2
  81. package/dist/host/accounts/deliver.js +4 -0
  82. package/dist/host/accounts/kinds/perplexityapp.d.ts +2 -0
  83. package/dist/host/accounts/kinds/perplexityapp.js +8 -0
  84. package/dist/host/accounts/kinds/unity.d.ts +1 -1
  85. package/dist/host/accounts/kinds/unity.js +60 -8
  86. package/dist/host/accounts/leases.d.ts +2 -1
  87. package/dist/host/accounts/sources.js +2 -0
  88. package/dist/host/apps/index.d.ts +1 -0
  89. package/dist/host/apps/index.js +2 -0
  90. package/dist/host/apps/store-package.d.ts +51 -0
  91. package/dist/host/apps/store-package.js +209 -0
  92. package/dist/host/github/deliver.d.ts +8 -3
  93. package/dist/host/github/deliver.js +42 -29
  94. package/dist/host/github/index.d.ts +1 -0
  95. package/dist/host/github/index.js +1 -0
  96. package/dist/host/github/pc-login.d.ts +27 -0
  97. package/dist/host/github/pc-login.js +129 -0
  98. package/dist/host/pc-installs/discover.d.ts +4 -0
  99. package/dist/host/pc-installs/discover.js +16 -4
  100. package/dist/host/pc-installs/source.d.ts +1 -1
  101. package/dist/host/pc-installs/unity-assets.d.ts +5 -0
  102. package/dist/host/pc-installs/unity-assets.js +18 -0
  103. package/dist/host/pc-installs/unity.d.ts +20 -0
  104. package/dist/host/pc-installs/unity.js +135 -2
  105. package/dist/runtime/agent.js +116 -11
  106. package/dist/runtime/daemon.js +2170 -976
  107. package/dist/runtime/db.js +116 -11
  108. package/dist/runtime/main.js +2543 -1364
  109. package/dist/runtime/netproxy.js +116 -11
  110. package/dist/runtime/tool.js +133 -31
  111. package/dist/runtime/view.js +116 -11
  112. package/dist/selftest/agents-web.test.js +2 -1
  113. package/dist/selftest/engines.test.js +2 -2
  114. package/dist/selftest/github.test.js +64 -1
  115. package/dist/selftest/index.js +10 -0
  116. package/dist/selftest/menu-screens.js +3 -1
  117. package/dist/selftest/persisted-installs.test.js +1 -1
  118. package/dist/selftest/service-accounts.test.js +1 -1
  119. package/dist/selftest/store-app.test.d.ts +2 -0
  120. package/dist/selftest/store-app.test.js +162 -0
  121. package/dist/selftest/support/engine-golden.js +7 -0
  122. package/dist/selftest/support/fixtures.d.ts +10 -1
  123. package/dist/selftest/support/unity-sample.d.ts +2 -0
  124. package/dist/selftest/support/unity-sample.js +80 -0
  125. package/dist/selftest/unity-accounts.test.d.ts +2 -0
  126. package/dist/selftest/unity-accounts.test.js +106 -0
  127. package/dist/selftest/unity-agent.test.d.ts +2 -0
  128. package/dist/selftest/unity-agent.test.js +47 -0
  129. package/dist/selftest/unity-page.test.d.ts +2 -0
  130. package/dist/selftest/unity-page.test.js +177 -0
  131. package/dist/selftest/unity-setup.test.d.ts +2 -0
  132. package/dist/selftest/unity-setup.test.js +49 -0
  133. package/dist/selftest/unity.test.js +238 -3
  134. package/dist/shared/accounts/index.d.ts +1 -0
  135. package/dist/shared/accounts/index.js +1 -0
  136. package/dist/shared/accounts/kinds/unity.d.ts +28 -0
  137. package/dist/shared/accounts/kinds/unity.js +88 -0
  138. package/dist/shared/accounts/kinds.d.ts +2 -2
  139. package/dist/shared/accounts/kinds.js +23 -1
  140. package/dist/shared/accounts/payloads.d.ts +52 -0
  141. package/dist/shared/accounts/payloads.js +14 -2
  142. package/dist/shared/apps/catalogue-agents-web.d.ts +7 -0
  143. package/dist/shared/apps/catalogue-agents-web.js +23 -0
  144. package/dist/shared/apps/catalogue-gamedev.d.ts +3 -0
  145. package/dist/shared/apps/catalogue-gamedev.js +31 -0
  146. package/dist/shared/apps/catalogue.d.ts +8 -0
  147. package/dist/shared/apps/index.d.ts +3 -2
  148. package/dist/shared/apps/index.js +3 -2
  149. package/dist/shared/apps/selection.d.ts +6 -0
  150. package/dist/shared/apps/selection.js +9 -0
  151. package/dist/shared/apps/store-package.d.ts +31 -0
  152. package/dist/shared/apps/store-package.js +34 -0
  153. package/dist/shared/channel/schema.d.ts +24 -0
  154. package/dist/shared/channel/secrets.d.ts +17 -0
  155. package/dist/shared/paths/pc-installs.d.ts +1 -1
  156. package/dist/shared/paths/pc-installs.js +1 -0
  157. package/dist/shared/readiness.d.ts +2 -2
  158. package/dist/shared/readiness.js +2 -2
  159. package/dist/shared/unity/asset-cache.d.ts +20 -0
  160. package/dist/shared/unity/asset-cache.js +44 -0
  161. package/dist/shared/unity/cli.d.ts +40 -0
  162. package/dist/shared/unity/cli.js +77 -0
  163. package/dist/shared/unity/editors.d.ts +46 -0
  164. package/dist/shared/unity/editors.js +89 -0
  165. package/dist/shared/unity/guide.d.ts +9 -0
  166. package/dist/shared/unity/guide.js +84 -0
  167. package/dist/shared/unity/index.d.ts +9 -0
  168. package/dist/shared/unity/index.js +11 -0
  169. package/dist/shared/unity/manifest.d.ts +10 -0
  170. package/dist/shared/unity/manifest.js +53 -0
  171. package/dist/shared/unity/mcp.d.ts +8 -0
  172. package/dist/shared/unity/mcp.js +12 -0
  173. package/dist/shared/unity/release.d.ts +28 -0
  174. package/dist/shared/unity/release.js +83 -0
  175. package/dist/shared/unity/status.d.ts +59 -0
  176. package/dist/shared/unity/status.js +45 -0
  177. package/dist/shared/unity/version.d.ts +27 -0
  178. package/dist/shared/unity/version.js +75 -0
  179. package/dist/web/index.html +1 -1
  180. package/dist/web/{main-FNE6XEHA.js → main-VPMYTSUL.js} +67 -67
  181. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  182. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  183. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  184. package/native/scripts/get-store-package.ps1 +58 -0
  185. package/package.json +1 -1
  186. package/sandbox/addons/openupm-cli/package-lock.json +2921 -0
  187. package/sandbox/addons/openupm-cli/package.json +7 -0
  188. package/sandbox/addons/unity-mcp/requirements.in +1 -0
  189. package/sandbox/addons/unity-mcp/requirements.txt +1165 -0
  190. package/sandbox/csharp/AppActivation.cs +38 -0
  191. package/sandbox/scripts/install-store-package.ps1 +28 -0
  192. package/sandbox/scripts/start-store-app.ps1 +29 -0
  193. package/schemas/config.schema.json +61 -6
  194. package/schemas/sandbox-config.schema.json +38 -5
  195. package/dist/guest/devtools/unity.d.ts +0 -2
  196. package/dist/guest/devtools/unity.js +0 -7
@@ -106,6 +106,7 @@ export declare function createFixtures(t: Harness): {
106
106
  };
107
107
  github: {
108
108
  shareToken: boolean;
109
+ usePcLogin: boolean;
109
110
  };
110
111
  accounts: {
111
112
  epic: {
@@ -138,6 +139,9 @@ export declare function createFixtures(t: Harness): {
138
139
  comet: {
139
140
  share: boolean;
140
141
  };
142
+ perplexityapp: {
143
+ share: boolean;
144
+ };
141
145
  };
142
146
  pcInstalls: {
143
147
  steamGames: boolean | number[];
@@ -145,7 +149,8 @@ export declare function createFixtures(t: Harness): {
145
149
  unrealEngines: boolean | string[] | "auto";
146
150
  unrealEngineFolders: string[];
147
151
  unrealAssets: boolean | "auto";
148
- unityEditors: boolean | string[];
152
+ unityEditors: boolean | string[] | "auto";
153
+ unityAssets: boolean | "auto";
149
154
  };
150
155
  provisioning: {
151
156
  copyFromHost: boolean;
@@ -212,6 +217,10 @@ export declare function createFixtures(t: Harness): {
212
217
  modules: string[];
213
218
  hub: boolean;
214
219
  keepVersions: number;
220
+ mcp: boolean;
221
+ agentSkills: boolean;
222
+ cliVersion: string;
223
+ assetCache: "pc" | "persisted" | "local";
215
224
  };
216
225
  godot: {
217
226
  enabled: boolean | "auto";
@@ -0,0 +1,2 @@
1
+ import type { UnityOverview } from '../../app/features/unity/index.js';
2
+ export declare const UNITY_SAMPLE: UnityOverview;
@@ -0,0 +1,80 @@
1
+ export const UNITY_SAMPLE = {
2
+ setUp: true,
3
+ checkedAt: '2026-11-05T12:00:00.000Z',
4
+ cli: { version: '1.0.0-beta.16', path: 'C:\\AgentData\\tools\\unity\\cli\\1.0.0-beta.16\\unity.exe' },
5
+ editors: [
6
+ {
7
+ version: '6000.0.58f2',
8
+ dir: 'C:\\PcInstalls\\unity\\6000.0.58f2',
9
+ where: 'this PC, read-only',
10
+ writable: false,
11
+ modules: ['android'],
12
+ sizeBytes: null,
13
+ projects: ['Platformer'],
14
+ },
15
+ {
16
+ version: '2022.3.20f1',
17
+ dir: 'C:\\AgentData\\tools\\unity\\editors\\2022.3.20f1',
18
+ where: 'the data disk, kept between sandboxes',
19
+ writable: true,
20
+ modules: [],
21
+ sizeBytes: 9 * 1024 ** 3,
22
+ projects: ['OldGame'],
23
+ },
24
+ ],
25
+ projects: [
26
+ {
27
+ project: 'Platformer',
28
+ folder: 'F:\\Code\\Platformer',
29
+ version: '6000.0.58f2',
30
+ editor: 'Unity 6000.0.58f2',
31
+ reason: 'the project asks for 6000.0.58f2',
32
+ bridge: 'pipeline',
33
+ pipeline: true,
34
+ },
35
+ {
36
+ project: 'OldGame',
37
+ folder: 'F:\\Code\\OldGame',
38
+ version: '2022.3.20f1',
39
+ editor: 'Unity 2022.3.20f1',
40
+ reason: 'the project asks for 2022.3.20f1',
41
+ bridge: null,
42
+ pipeline: false,
43
+ },
44
+ ],
45
+ licence: { state: 'active', kind: 'Personal', detail: 'Unity Personal', user: 'dev@example.com' },
46
+ hub: { installed: false, setting: false },
47
+ assetCache: { mode: 'persisted', dir: 'C:\\AgentData\\tools\\unity\\asset-store' },
48
+ agentTools: { cliVersion: '1.0.0-beta.16', mcp: 'sbx-unity', mcpSetting: true, skills: true, skillsSetting: true },
49
+ pc: {
50
+ setting: 'auto',
51
+ editors: [{ version: '6000.0.58f2', folder: 'C:\\Program Files\\Unity\\Hub\\Editor\\6000.0.58f2' }],
52
+ problem: null,
53
+ },
54
+ assets: {
55
+ setting: 'auto',
56
+ folders: [
57
+ {
58
+ folder: 'C:\\Users\\dev\\AppData\\Roaming\\Unity\\Asset Store-5.x',
59
+ packages: [
60
+ { publisher: 'Kenney', category: '2D', name: 'Platformer Pack', size: '120 MB' },
61
+ { publisher: 'Synty Studios', category: '3D Models', name: 'POLYGON Fantasy', size: '1.4 GB' },
62
+ ],
63
+ more: 0,
64
+ },
65
+ ],
66
+ },
67
+ problem: null,
68
+ settings: {
69
+ enabled: 'auto',
70
+ acceptLicense: true,
71
+ version: 'project',
72
+ modules: 'none',
73
+ hub: false,
74
+ keepVersions: 2,
75
+ mcp: true,
76
+ agentSkills: true,
77
+ cliVersion: '1.0.0-beta.16',
78
+ assetCache: 'persisted',
79
+ },
80
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const unityAccountsSuite: Suite;
@@ -0,0 +1,106 @@
1
+ // Unity licence and sign-in: the host source (a fake ProgramData and LocalAppData in temp folders), the guest adapter
2
+ // (apply and read back, release with an injected runner) and the Accounts page. Nothing real is read or run.
3
+ import path from 'node:path';
4
+ import { serviceRow, servicePage } from '../app/features/accounts/index.js';
5
+ import { pageLines } from '../cli/output/index.js';
6
+ import { AccountLeases, HOST_SOURCES, ServiceAccountStore } from '../host/accounts/index.js';
7
+ import { unitySource } from '../host/accounts/kinds/unity.js';
8
+ import { SecretStore } from '../host/secrets/index.js';
9
+ import { serviceWanted, unityEntitlementFile, unityUlfFile, } from '../shared/accounts/index.js';
10
+ import { activateUnityLicence, unityAdapterFor } from '../guest/accounts/kinds/unity.js';
11
+ const ULF = '<?xml version="1.0"?><root><License id="Terms"><SerialMasked Value="U3-XXXX-XXXX"/></License></root>';
12
+ const ENTITLEMENT = '<?xml version="1.0"?><Entitlement><Id>abc</Id></Entitlement>';
13
+ const PASSWORD = 'hunter2-unity-pw';
14
+ const SERIAL = 'E3-AAAA-BBBB-CCCC-DDDD-EEEE';
15
+ function message(fn) {
16
+ try {
17
+ fn();
18
+ }
19
+ catch (e) {
20
+ return String(e.message);
21
+ }
22
+ return '';
23
+ }
24
+ export const unityAccountsSuite = {
25
+ name: 'unity-accounts',
26
+ title: 'Unity licence and sign-in',
27
+ run: async (t, f) => {
28
+ t.section('the host source');
29
+ const pcDir = t.dir('unity-pc');
30
+ const pc = {
31
+ programData: path.join(pcDir, 'ProgramData'),
32
+ localAppData: path.join(pcDir, 'Local'),
33
+ appData: path.join(pcDir, 'Roaming'),
34
+ };
35
+ t.check(unitySource.fromPc(pc) === null, 'a PC with no Unity licence gives nothing');
36
+ t.write(unityUlfFile(pc.programData), ULF);
37
+ t.write(unityEntitlementFile(pc.localAppData), ENTITLEMENT);
38
+ const fromPc = unitySource.fromPc(pc);
39
+ t.check(fromPc?.ulf === ULF && fromPc.entitlement === ENTITLEMENT, 'the PC licence and entitlement are read');
40
+ const other = t.dir('unity-other');
41
+ t.write(unityUlfFile(other), ULF);
42
+ t.check(unitySource.fromPc({ ...pc, localAppData: '' }, other)?.ulf === ULF, '--from names another ProgramData folder');
43
+ const virtual = t.dir('unity-virtual');
44
+ t.write(path.join(virtual, 'VirtualStore', 'ProgramData', 'Unity', 'Unity_lic.ulf'), ULF);
45
+ t.check(unitySource.fromPc({ programData: path.join(virtual, 'none'), localAppData: virtual, appData: '' })?.ulf === ULF, 'the VirtualStore copy is the fallback');
46
+ const refusal = message(() => unitySource.fromFile(Buffer.from('hello'), {}));
47
+ t.check(refusal.includes('That is not a Unity licence file (Unity_lic.ulf) or entitlement file.'), 'a file that is not a licence is refused with the plain text', refusal);
48
+ const ulfPayload = unitySource.fromFile(Buffer.from(ULF), { email: 'me@example.com' });
49
+ const entPayload = unitySource.fromFile(Buffer.from(ENTITLEMENT), {});
50
+ t.check(ulfPayload.ulf === ULF && ulfPayload.email === 'me@example.com' && entPayload.entitlement === ENTITLEMENT, 'a ULF becomes ulf, an Entitlement file becomes entitlement, answers are added');
51
+ t.check(message(() => unitySource.fromAnswers({ serial: SERIAL, email: 'me@example.com' })).includes('A serial needs your Unity ID email and password.'), 'a serial without email and password is refused');
52
+ const typed = unitySource.fromAnswers({ email: 'me@example.com', password: PASSWORD, serial: SERIAL });
53
+ t.check(typed.email === 'me@example.com' && typed.password === PASSWORD && typed.serial === SERIAL, 'typed answers');
54
+ const summary = unitySource.summary(typed) ?? '';
55
+ t.check(!summary.includes(PASSWORD) && !summary.includes(SERIAL), 'the summary never holds a secret');
56
+ t.section('the guest adapter');
57
+ const runs = [];
58
+ const run = (file, args) => {
59
+ runs.push(file + ' ' + args.join(' '));
60
+ return Promise.resolve({ code: 0, stdout: '{"status":"active","type":"Personal"}' });
61
+ };
62
+ const dirs = {
63
+ programData: t.dir('unity-guest-pd'),
64
+ localAppData: t.dir('unity-guest-local'),
65
+ secrets: t.dir('unity-guest-secrets'),
66
+ };
67
+ const adapter = unityAdapterFor(dirs, run);
68
+ t.check(adapter.current() === null, 'nothing is held at first');
69
+ const payload = { ulf: ULF, entitlement: ENTITLEMENT, email: 'me@example.com', password: PASSWORD };
70
+ const applied = await adapter.apply(payload);
71
+ const back = adapter.current();
72
+ t.check(back?.ulf === ULF &&
73
+ back.entitlement === ENTITLEMENT &&
74
+ back.email === 'me@example.com' &&
75
+ back.password === PASSWORD, 'apply then current keeps the licence files and the password');
76
+ t.check(!applied.summary.includes(PASSWORD) && !applied.summary.includes('me@example.com'), 'the summary is safe');
77
+ await adapter.release();
78
+ t.check(runs.length === 0, 'release without activated.json runs nothing');
79
+ const serialDirs = { ...dirs, secrets: t.dir('unity-guest-secrets2') };
80
+ const serialRuns = [];
81
+ const record = (file, args, o) => {
82
+ serialRuns.push(file + ' ' + args.join(' '));
83
+ return run(file, args, o);
84
+ };
85
+ const serialAdapter = unityAdapterFor(serialDirs, record);
86
+ await serialAdapter.apply({ email: 'me@example.com', password: PASSWORD, serial: SERIAL });
87
+ const licence = await activateUnityLicence({ editorExe: 'C:\\Editor\\Unity.exe', unityExe: 'C:\\Cli\\unity.exe' }, serialDirs, record);
88
+ t.check(serialRuns[0].includes('-serial') && serialRuns[0].includes('activate.log') && licence.state === 'active', 'a serial is activated with the editor, then the licence status is read');
89
+ t.check(!JSON.stringify(licence).includes(PASSWORD) && !JSON.stringify(licence).includes(SERIAL), 'the licence status holds no secret');
90
+ await serialAdapter.release();
91
+ t.check(serialRuns.some((r) => r.includes('-returnlicense') && r.includes('return.log')), 'release returns the seat once activated.json exists');
92
+ const noLicence = () => Promise.resolve({ code: 0, stdout: '{"licenses":[]}' });
93
+ const none = await activateUnityLicence({ editorExe: null, unityExe: 'C:\\Cli\\unity.exe' }, { programData: t.dir('unity-empty-pd'), localAppData: t.dir('unity-empty-la'), secrets: t.dir('unity-empty-s') }, noLicence);
94
+ t.check(none.state === 'none' && none.detail === 'none: sign in to Unity once in this sandbox', 'no licence: sign in once');
95
+ const refused = await activateUnityLicence({ editorExe: null, unityExe: 'C:\\Cli\\unity.exe', source: 'this PC' }, dirs, noLicence);
96
+ t.check(refused.state === 'invalid' && refused.detail.includes('is not accepted here: sign in again'), 'a licence file that is not accepted says to sign in again');
97
+ t.section('who wants it, and the page');
98
+ t.check(serviceWanted('unity', new Set(['unity'])) && !serviceWanted('unity', new Set(['steam'])), 'only the Unity toolchain wants the Unity sign-in');
99
+ t.check(HOST_SOURCES.unity === unitySource, 'the Unity source is registered');
100
+ const config = f.makeConfig();
101
+ const store = new ServiceAccountStore(config.storage.root, SecretStore.memory());
102
+ const row = serviceRow('unity', config, store, new AccountLeases(config.storage.root));
103
+ const text = pageLines(servicePage(row)).join('\n');
104
+ t.check(row.canImport && row.canLogin && row.warnings.length > 0 && !text.includes('sandbox account'), 'the Accounts section offers import and sign in, and shows no "sandbox account" command text', text);
105
+ },
106
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const unityAgentSuite: Suite;
@@ -0,0 +1,47 @@
1
+ // Unity agent tools: sbx-unity in Claude Code's and Codex's MCP settings, the Apps add-ons (MCP for Unity, openupm-cli)
2
+ // and their lock files. Pure: nothing is installed or run.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { addonMcpServer, APPS, codexWithAddonMcpServers, withAddonMcpServers, } from '../shared/apps/index.js';
6
+ import { UNITY_MCP_NAME, unityCliMcpServer } from '../shared/unity/index.js';
7
+ export const unityAgentSuite = {
8
+ name: 'unity-agent',
9
+ title: 'Unity agent tools: MCP, guide, Apps add-ons',
10
+ run: (t) => {
11
+ t.section('sbx-unity in the agents');
12
+ const server = unityCliMcpServer('C:\\AgentData\\tools\\unity\\cli\\1.0.0\\unity.exe', 'C:\\Workspace\\game');
13
+ const mine = { mine: { type: 'stdio', command: 'mine.exe', args: [], env: {} } };
14
+ // The guest's ENGINE_MCP_NAMES (guest/setup/addon-mcp.ts) cannot be imported here: it loads the guest runtime.
15
+ const engineNames = ['sbx-unreal-editor', UNITY_MCP_NAME];
16
+ const names = [...APPS.flatMap((a) => (a.mcp ? [a.mcp.name] : [])), ...engineNames];
17
+ const on = withAddonMcpServers(mine, { [UNITY_MCP_NAME]: server }, names);
18
+ t.check('mine' in on && UNITY_MCP_NAME in on, 'your own servers are kept and sbx-unity is added');
19
+ const off = withAddonMcpServers(on, {}, names);
20
+ t.check('mine' in off && !(UNITY_MCP_NAME in off), 'sbx-unity is removed when it is off; your servers stay');
21
+ const codex = codexWithAddonMcpServers('', { [UNITY_MCP_NAME]: server }, engineNames);
22
+ t.check(codex.toml.includes('[mcp_servers.sbx-unity]') && codex.toml.includes('args = ["mcp", "--cli-tools"]'), 'Codex gets [mcp_servers.sbx-unity] with args = ["mcp", "--cli-tools"]');
23
+ t.section('Apps add-ons');
24
+ const unity = APPS.find((a) => a.id === 'unity-mcp');
25
+ const built = addonMcpServer('C:\\x', unity.mcp, { exe: 'C:\\x\\venv\\Scripts\\mcp-for-unity.exe' });
26
+ t.check(built?.type === 'stdio' &&
27
+ built.command === 'C:\\x\\venv\\Scripts\\mcp-for-unity.exe' &&
28
+ built.args.join(' ') === '--transport stdio' &&
29
+ built.env.DISABLE_TELEMETRY === '1' &&
30
+ built.env.UNITY_MCP_DISABLE_TELEMETRY === '1' &&
31
+ built.env.MCP_DISABLE_TELEMETRY === '1' &&
32
+ unity.mcp?.name === 'sbx-unity-mcp' &&
33
+ unity.mcp.codex === true, 'MCP for Unity runs mcp-for-unity.exe --transport stdio with telemetry off, for Claude Code and Codex');
34
+ const openupm = APPS.find((a) => a.id === 'openupm-cli');
35
+ t.check(openupm?.exe === 'openupm.cmd' && openupm.category === 'gamedev' && !openupm.mcp, 'openupm-cli is a game-development app');
36
+ const addons = path.join(process.cwd(), 'sandbox', 'addons');
37
+ const reqs = fs.readFileSync(path.join(addons, 'unity-mcp', 'requirements.txt'), 'utf8');
38
+ t.check(fs
39
+ .readFileSync(path.join(addons, 'unity-mcp', 'requirements.in'), 'utf8')
40
+ .includes('mcpforunityserver==10.3.0') &&
41
+ /^mcpforunityserver==10\.3\.0 \\\r?\n\s+--hash=sha256:[0-9a-f]{64}/m.test(reqs), 'the MCP for Unity lock file pins mcpforunityserver 10.3.0 with hashes');
42
+ const lock = fs.readFileSync(path.join(addons, 'openupm-cli', 'package-lock.json'), 'utf8');
43
+ t.check(fs.existsSync(path.join(addons, 'openupm-cli', 'package.json')) &&
44
+ lock.includes('"node_modules/openupm-cli"') &&
45
+ lock.includes('"version": "4.5.3"'), 'the openupm-cli lock file pins 4.5.3');
46
+ },
47
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const unityPageSuite: Suite;
@@ -0,0 +1,177 @@
1
+ // The Unity page and its host side: this PC's editors and Asset Store downloads (a temp PC tree), "auto" following the
2
+ // launch, the status file never read through a link, the page itself (no command text, every operation on it) and the
3
+ // editor bridge edited in a temp project. No network: the Pipeline version lookup is given a fake.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { DEFAULTS } from '../core/config/index.js';
7
+ import { pageActions } from '../app/kernel/index.js';
8
+ import { unityPage, unityView } from '../app/features/unity/index.js';
9
+ import { addBridgeToProject, latestPipelineVersion, readStatus, removeBridgeFromProject, } from '../app/features/unity/unity.run.js';
10
+ import { discoverPcInstalls } from '../host/pc-installs/index.js';
11
+ import { findUnityEditorsOnPc, unityAssetCacheDirs, unityPcFiles } from '../host/pc-installs/unity.js';
12
+ import { unityAssetInstalls } from '../host/pc-installs/unity-assets.js';
13
+ import { pageLines } from '../cli/output/render-view.js';
14
+ import { UNITY_STATUS_FILE, bridgeOf } from '../shared/unity/index.js';
15
+ import { UNITY_SAMPLE } from './support/unity-sample.js';
16
+ const STATUS = {
17
+ updatedAt: '2026-11-05T12:00:00.000Z',
18
+ cli: null,
19
+ editors: [],
20
+ projects: [],
21
+ licence: { state: 'unknown', kind: '', detail: '', user: null },
22
+ assetCache: { mode: 'persisted', dir: null },
23
+ pcAssets: [],
24
+ hub: { installed: false, path: null },
25
+ mcp: null,
26
+ skills: false,
27
+ problem: null,
28
+ };
29
+ const MANIFEST = JSON.stringify({ dependencies: { 'com.unity.ugui': '2.0.0', 'com.unity.modules.ai': '1.0.0' } }, null, 2) + '\n';
30
+ export const unityPageSuite = {
31
+ name: 'unity-page',
32
+ title: "The Unity page: this PC's editors and downloads, auto, the status file, the bridge",
33
+ run: async (t) => {
34
+ t.section("this PC's editors");
35
+ const pcDir = t.dir('pc');
36
+ const appData = path.join(pcDir, 'AppData');
37
+ const programFiles = path.join(pcDir, 'Program Files');
38
+ const editor = (root) => t.write(path.join(root, 'Editor', 'Unity.exe'), 'x');
39
+ editor(path.join(programFiles, 'Unity', 'Hub', 'Editor', '6000.0.58f2'));
40
+ const second = path.join(pcDir, 'Editors');
41
+ editor(path.join(second, '2022.3.20f1'));
42
+ t.write(path.join(appData, 'UnityHub', 'secondaryInstallPath.json'), JSON.stringify(second));
43
+ const listedRoot = path.join(pcDir, 'Elsewhere', '2021.3.1f1');
44
+ editor(listedRoot);
45
+ editor(path.join(pcDir, 'Elsewhere', 'evil'));
46
+ t.write(path.join(appData, 'UnityHub', 'editors-v2.json'), JSON.stringify({
47
+ 'schema-version': 'v2',
48
+ data: [
49
+ { version: '2021.3.1f1', location: [path.join(listedRoot, 'Editor', 'Unity.exe')], other: 1 },
50
+ { version: '../../evil', location: path.join(pcDir, 'Elsewhere', 'evil', 'Editor', 'Unity.exe') },
51
+ { version: '2020.3.1f1', location: path.join(pcDir, 'Elsewhere', 'x', 'Editor', 'notunity.exe') },
52
+ { version: 5, location: 3 },
53
+ ],
54
+ }));
55
+ const files = unityPcFiles({ appData, localAppData: '', programData: '', programFiles });
56
+ const found = findUnityEditorsOnPc(files, true);
57
+ t.equal(found.map((e) => e.id), ['6000.0.58f2', '2022.3.20f1', '2021.3.1f1'], 'the default folder, the secondary folder and editors-v2.json are read; a bad version or exe name is skipped');
58
+ t.check(found.every((e) => e.kind === 'unity' && e.name === 'Unity ' + e.id && e.version === e.id), 'each editor is named by its version');
59
+ t.equal(findUnityEditorsOnPc(files, ['2022.3.20f1']).map((e) => e.id), ['2022.3.20f1'], 'a list asks for exact versions only');
60
+ t.write(path.join(appData, 'UnityHub', 'secondaryInstallPath.json'), JSON.stringify('relative\\dir'));
61
+ t.equal(findUnityEditorsOnPc(files, true).map((e) => e.id), ['6000.0.58f2', '2021.3.1f1'], 'a relative secondary path is ignored');
62
+ t.section("this PC's Asset Store downloads");
63
+ const store = path.join(appData, 'Unity', 'Asset Store-5.x');
64
+ t.write(path.join(store, 'Kenney', '2D', 'Pack.unitypackage'), 'x');
65
+ const envRoot = t.dir('pc', 'Cache');
66
+ t.dir('pc', 'Cache', 'Asset Store-5.x');
67
+ t.equal(unityAssetCacheDirs(files, undefined), [store], 'the default folder under %APPDATA%\\Unity');
68
+ t.equal(unityAssetCacheDirs(files, envRoot), [path.join(envRoot, 'Asset Store-5.x'), store], 'the environment variable first, then the default');
69
+ t.equal(unityAssetInstalls([store, store + '2']).map((i) => i.id), ['AssetStore', 'AssetStore2'], 'mapped at AssetStore, AssetStore2');
70
+ t.section('auto follows the launch');
71
+ const host = t.dir('auto-host', '6000.0.58f2');
72
+ const unitySource = {
73
+ kind: 'unity',
74
+ setting: 'unityEditors',
75
+ find: () => [{ kind: 'unity', id: '6000.0.58f2', name: 'Unity 6000.0.58f2', host, guest: '' }],
76
+ };
77
+ const unrealSource = {
78
+ kind: 'unreal',
79
+ setting: 'unrealEngines',
80
+ find: () => [{ kind: 'unreal', id: 'UE_5.4', name: 'Unreal Engine 5.4', host, guest: '' }],
81
+ };
82
+ const run = (o) => discoverPcInstalls(DEFAULTS.pcInstalls, {
83
+ packageRoot: t.packageRoot,
84
+ storageRoot: t.dir('auto-storage'),
85
+ sources: [unitySource, unrealSource],
86
+ ...o,
87
+ }).manifest.items.map((i) => i.guest);
88
+ t.equal(run({ unityOn: true }), ['C:\\PcInstalls\\unity\\6000.0.58f2'], 'Unity on: the editors, not the engines');
89
+ t.equal(run({ unrealOn: true }), ['C:\\PcInstalls\\unreal\\UE_5.4'], 'Unreal on: the engines, not the editors');
90
+ t.equal(run({}), [], 'neither on: nothing');
91
+ t.section('the status file');
92
+ const data = t.dir('status-data');
93
+ const target = t.dir('status-elsewhere');
94
+ t.write(path.join(target, UNITY_STATUS_FILE), JSON.stringify(STATUS));
95
+ t.write(path.join(data, 'plain', 'tools', 'unity', UNITY_STATUS_FILE), JSON.stringify(STATUS));
96
+ t.check(readStatus(path.join(data, 'plain')) !== null, 'a status file in the data folder is read');
97
+ fs.mkdirSync(path.join(data, 'linked', 'tools'), { recursive: true });
98
+ fs.symlinkSync(target, path.join(data, 'linked', 'tools', 'unity'), 'junction');
99
+ t.check(readStatus(path.join(data, 'linked')) === null, 'a junction at tools\\unity is never followed');
100
+ t.write(path.join(data, 'bad', 'tools', 'unity', UNITY_STATUS_FILE), '{"editors": "x"}');
101
+ t.check(readStatus(path.join(data, 'bad')) === null, 'a status file that does not fit the schema is not used');
102
+ t.section('the page');
103
+ const page = unityPage(UNITY_SAMPLE);
104
+ t.equal(page.sections.map((s) => s.id), ['editors', 'pc', 'assets', 'licence', 'projects', 'tools', 'settings'], 'editors, this PC, assets, licence, projects, agent tools, settings');
105
+ const actions = pageActions(page);
106
+ const ops = actions.flatMap((a) => (a.kind === 'operation' ? [a.op] : []));
107
+ t.check(unityView.operations.every((op) => ops.includes(op.id) || op.id === 'unity.openHub'), 'every operation of the view is on the page (Open Unity Hub once the Hub is in the sandbox)');
108
+ const withHub = pageActions(unityPage({ ...UNITY_SAMPLE, hub: { installed: true, setting: true } }));
109
+ t.check(withHub.some((a) => a.kind === 'operation' && a.op === 'unity.openHub'), 'Open Unity Hub is offered when the Hub is installed');
110
+ t.check(actions.some((a) => a.kind === 'link' && a.target.kind === 'view' && a.target.view === 'accounts'), 'a link to Unity sign-in in Accounts');
111
+ const settingKeys = actions.flatMap((a) => a.kind === 'link' && a.target.kind === 'setting' ? [a.target.key] : []);
112
+ t.check(settingKeys.includes('pcInstalls.unityEditors'), 'the settings are links');
113
+ const text = pageLines(page).join('\n');
114
+ for (const needle of [
115
+ '6000.0.58f2',
116
+ 'this PC, read-only',
117
+ 'Platformer Pack',
118
+ 'dev@example.com',
119
+ 'toolchains.unity.assetCache',
120
+ 'pcInstalls.unityAssets',
121
+ ])
122
+ t.includes(text, needle, 'the page shows ' + needle);
123
+ t.check(!/sandbox (unity|launch|config|account|projects)/.test(text), 'no page text tells you to run a command');
124
+ t.excludes(text, 'unity editors', 'no Unity CLI command on the page');
125
+ t.excludes(text, ' --', 'no flag on the page');
126
+ const empty = pageLines(unityPage({ ...UNITY_SAMPLE, setUp: false, editors: [], projects: [] })).join('\n');
127
+ t.includes(empty, 'No sandbox has set Unity up yet', 'nothing set up: the page says so');
128
+ t.section('the editor bridge');
129
+ const project = t.dir('bridge', 'Game');
130
+ t.write(path.join(project, 'ProjectSettings', 'ProjectVersion.txt'), 'm_EditorVersion: 6000.0.58f2\n');
131
+ const manifest = t.write(path.join(project, 'Packages', 'manifest.json'), MANIFEST);
132
+ const hooks = {
133
+ pipelineVersion: () => Promise.resolve('0.10.1'),
134
+ confirm: () => Promise.resolve(true),
135
+ };
136
+ const declined = await addBridgeToProject(project, undefined, { ...hooks, confirm: () => Promise.resolve(false) });
137
+ t.check(fs.readFileSync(manifest, 'utf8') === MANIFEST && declined.message === 'Nothing was changed.', 'no: nothing changes');
138
+ const added = await addBridgeToProject(project, undefined, hooks);
139
+ const after = JSON.parse(fs.readFileSync(manifest, 'utf8'));
140
+ t.equal(Object.keys(after.dependencies), ['com.unity.ugui', 'com.unity.modules.ai', 'com.unity.pipeline'], 'exactly one dependency is added, the others keep their order');
141
+ t.check(after.dependencies['com.unity.pipeline'] === '0.10.1', 'the Pipeline version is the one looked up');
142
+ t.check(bridgeOf(fs.readFileSync(manifest, 'utf8')) === 'pipeline' && added.file === manifest, 'it is the Pipeline bridge');
143
+ const again = await addBridgeToProject(project, undefined, hooks);
144
+ t.includes(again.message, 'already', 'adding twice changes nothing');
145
+ await removeBridgeFromProject(project, hooks);
146
+ t.check(fs.readFileSync(manifest, 'utf8') === MANIFEST, 'removing restores the bytes');
147
+ const crlf = MANIFEST.replace(/\n/g, '\r\n');
148
+ fs.writeFileSync(manifest, crlf);
149
+ await addBridgeToProject(project, undefined, hooks);
150
+ await removeBridgeFromProject(project, hooks);
151
+ t.check(fs.readFileSync(manifest, 'utf8') === crlf, 'a CRLF manifest comes back as it was');
152
+ const old = t.dir('bridge', 'Old');
153
+ t.write(path.join(old, 'ProjectSettings', 'ProjectVersion.txt'), 'm_EditorVersion: 2022.3.20f1\n');
154
+ const oldManifest = t.write(path.join(old, 'Packages', 'manifest.json'), MANIFEST);
155
+ await addBridgeToProject(old, undefined, hooks);
156
+ t.check(bridgeOf(fs.readFileSync(oldManifest, 'utf8')) === 'coplay', 'Unity before 6 gets the Coplay bridge');
157
+ await t.throws(() => addBridgeToProject(t.dir('bridge', 'Old2'), 'pipeline', hooks), /not a Unity project/, 'a folder without ProjectVersion.txt is refused');
158
+ const old2 = t.dir('bridge', 'Old3');
159
+ t.write(path.join(old2, 'ProjectSettings', 'ProjectVersion.txt'), 'm_EditorVersion: 2022.3.20f1\n');
160
+ t.write(path.join(old2, 'Packages', 'manifest.json'), MANIFEST);
161
+ await t.throws(() => addBridgeToProject(old2, 'pipeline', hooks), /Unity 6/, 'Pipeline is refused before Unity 6');
162
+ const offline = t.dir('bridge', 'Offline');
163
+ t.write(path.join(offline, 'ProjectSettings', 'ProjectVersion.txt'), 'm_EditorVersion: 6000.0.58f2\n');
164
+ const offlineManifest = t.write(path.join(offline, 'Packages', 'manifest.json'), MANIFEST);
165
+ const fallback = await addBridgeToProject(offline, undefined, {
166
+ ...hooks,
167
+ pipelineVersion: () => Promise.reject(new Error('offline')),
168
+ });
169
+ t.includes(fallback.message, 'offline', 'no answer from Unity: the bundled version is used and it says so');
170
+ t.check(bridgeOf(fs.readFileSync(offlineManifest, 'utf8')) === 'pipeline', 'the bridge is still added');
171
+ t.section('the Pipeline version lookup (a fake registry)');
172
+ const reply = (body) => () => Promise.resolve(new Response(body));
173
+ t.check((await latestPipelineVersion(reply(JSON.stringify({ 'dist-tags': { latest: '0.11.0' } })))) === '0.11.0', 'dist-tags.latest is used');
174
+ await t.throws(() => latestPipelineVersion(reply(JSON.stringify({ 'dist-tags': { latest: '1; rm -rf' } }))), /no usable version/, 'a version that is not semver is refused');
175
+ await t.throws(() => latestPipelineVersion(reply('x'.repeat(3 * 1024 * 1024))), /over 2 MB/, 'a huge answer is refused');
176
+ },
177
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const unitySetupSuite: Suite;
@@ -0,0 +1,49 @@
1
+ // Unity set-up on the data disk: an editor is installed into tools\unity\editors\<version> through ensurePersisted (a
2
+ // fake installer in a temp folder stands in for the Unity CLI), kept, pruned; sbx-unity is gone. Nothing is downloaded.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { ensurePersisted } from '../guest/toolchains/persisted.js';
6
+ const GB = 1024 ** 3;
7
+ export const unitySetupSuite = {
8
+ name: 'unity-setup',
9
+ title: 'Unity set-up on the data disk: editors kept between sandboxes, no sbx-unity helper',
10
+ run: async (t) => {
11
+ t.section('an editor on the data disk');
12
+ const tools = t.dir('unity-tools');
13
+ const deps = { toolsDir: tools, statfs: () => ({ bavail: 100 * GB, bsize: 1 }) };
14
+ const spec = (version) => ({
15
+ name: 'unity/editors',
16
+ version,
17
+ needBytes: 10 * GB,
18
+ marker: 'Editor\\Unity.exe',
19
+ what: 'Unity ' + version,
20
+ keepVersions: 2,
21
+ });
22
+ const fake = (version) => async (staging, temp) => {
23
+ fs.writeFileSync(path.join(temp, 'download.part'), 'x');
24
+ fs.mkdirSync(path.join(staging, 'Editor'), { recursive: true });
25
+ fs.writeFileSync(path.join(staging, 'Editor', 'Unity.exe'), 'unity ' + version);
26
+ };
27
+ const first = await ensurePersisted(spec('6000.0.58f2'), fake('6000.0.58f2'), deps);
28
+ t.check(first.fresh && first.dir === path.join(tools, 'unity', 'editors', '6000.0.58f2'), 'the editor goes to tools\\unity\\editors\\<version>');
29
+ let ran = false;
30
+ const second = await ensurePersisted(spec('6000.0.58f2'), async () => {
31
+ ran = true;
32
+ }, deps);
33
+ t.check(!second.fresh && !ran, 'a second call finds it and installs nothing');
34
+ await t.throws(() => ensurePersisted(spec('6000.0.60f1'), async () => {
35
+ throw new Error('the CLI failed');
36
+ }, deps), 'the CLI failed', 'a failed install is reported');
37
+ const editors = path.join(tools, 'unity', 'editors');
38
+ t.equal(fs.readdirSync(editors).filter((n) => n.startsWith('.staging-')).length, 0, 'a failed install leaves no .staging-* folder');
39
+ await ensurePersisted(spec('6000.0.59f1'), fake('6000.0.59f1'), deps);
40
+ await ensurePersisted(spec('6000.1.0f1'), fake('6000.1.0f1'), deps);
41
+ const kept = fs.readdirSync(editors).sort();
42
+ t.check(kept.length === 2 && kept.includes('6000.1.0f1') && !kept.includes('6000.0.58f2'), 'keepVersions 2 removes the least recently used editor');
43
+ t.section('no sbx-unity helper');
44
+ t.check(!fs.existsSync(path.join(t.packageRoot, 'src', 'guest', 'devtools', 'unity.ts')), 'devtools/unity.ts is gone');
45
+ const toolJs = path.join(t.packageRoot, 'dist', 'runtime', 'tool.js');
46
+ t.check(!fs.existsSync(toolJs) ||
47
+ !fs.readFileSync(toolJs, 'utf8').includes('build with -executeMethod, the licence status'), 'runtime/tool.js has no sbx-unity helper (the setting texts may still name the MCP server)');
48
+ },
49
+ };