gdharness 1.3.45 → 1.3.47

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.
package/build/cli.js CHANGED
@@ -68,6 +68,124 @@ var init_errors = __esm(() => {
68
68
  };
69
69
  });
70
70
 
71
+ // src/paths.ts
72
+ import { realpathSync, statSync } from "node:fs";
73
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
74
+ function realAsFarAsItExists(path) {
75
+ const missing = [];
76
+ let at = path;
77
+ for (;; ) {
78
+ try {
79
+ return join(realpathSync.native(at), ...missing);
80
+ } catch {
81
+ const parent = dirname(at);
82
+ if (parent === at) {
83
+ return path;
84
+ }
85
+ missing.unshift(basename(at));
86
+ at = parent;
87
+ }
88
+ }
89
+ }
90
+ function isSameDirectory(onePath, otherPath) {
91
+ return relative(realAsFarAsItExists(resolve(onePath)), realAsFarAsItExists(resolve(otherPath))) === "";
92
+ }
93
+ function isWithinRoot(rootPath, candidatePath) {
94
+ const step = relative(resolve(rootPath), resolve(candidatePath));
95
+ return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
96
+ }
97
+ function keptInside(given) {
98
+ const at = given.indexOf("::");
99
+ const inner = at === -1 ? "" : given.slice(at + 2);
100
+ return KEPT_ID.test(inner) ? { file: given.slice(0, at), inner } : { file: given, inner: undefined };
101
+ }
102
+ function resolveWithinProject(projectPath, candidatePath, options = {}) {
103
+ const trimmed = candidatePath.trim();
104
+ if (trimmed === "") {
105
+ return { ok: false, reason: "Path is empty." };
106
+ }
107
+ if (trimmed.includes("\x00")) {
108
+ return { ok: false, reason: "Path contains a null byte." };
109
+ }
110
+ const root = resolve(projectPath);
111
+ const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : isAbsolute(trimmed) && isWithinRoot(root, trimmed) ? relative(root, resolve(trimmed)).split(sep).join("/") : trimmed;
112
+ if (withoutScheme.startsWith("uid://")) {
113
+ return {
114
+ ok: false,
115
+ reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
116
+ };
117
+ }
118
+ const scheme = OTHER_SCHEME.exec(withoutScheme);
119
+ if (scheme) {
120
+ const elsewhere = scheme[0].toLowerCase() === "user://" ? " A game's user:// is a directory of its own outside the project: a running game's is under userDir in editor_status runtimes and in editor_run start's runtime, where a file reader of your own reads it." : "";
121
+ return {
122
+ ok: false,
123
+ reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.${elsewhere}`
124
+ };
125
+ }
126
+ if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
127
+ const tail = withoutScheme.split(/[\\/]+/).filter((step) => step !== "" && !step.endsWith(":"));
128
+ const besideIt = isAbsolute(trimmed) && isWithinRoot(realAsFarAsItExists(dirname(resolve(trimmed))), realAsFarAsItExists(root));
129
+ const example = tail.slice(besideIt ? -1 : -2).join("/") || "scenes/main.tscn";
130
+ return {
131
+ ok: false,
132
+ reason: `Path '${trimmed}' is absolute and outside the project; give a path inside it, such as '${example}'.`
133
+ };
134
+ }
135
+ const kept = keptInside(trimmed);
136
+ if (kept.inner !== undefined) {
137
+ return {
138
+ ok: false,
139
+ reason: `Path '${trimmed}' names the resource ${kept.inner} kept inside ${kept.file}, not a file. resource_edit read reads one by that path; this argument takes a file.`
140
+ };
141
+ }
142
+ if (withoutScheme.includes(":")) {
143
+ return {
144
+ ok: false,
145
+ reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
146
+ };
147
+ }
148
+ const absolutePath = resolve(root, withoutScheme);
149
+ if (!isWithinRoot(root, absolutePath)) {
150
+ return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
151
+ }
152
+ const relativePath = relative(root, absolutePath).split(sep).join("/");
153
+ if (relativePath === "" && options.root !== true) {
154
+ return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
155
+ }
156
+ return { ok: true, absolutePath, relativePath };
157
+ }
158
+ function exportedBuild(projectPath, candidatePath) {
159
+ const found = resolveWithinProject(projectPath, candidatePath);
160
+ if (!found.ok) {
161
+ return found;
162
+ }
163
+ let real;
164
+ try {
165
+ real = realpathSync.native(found.absolutePath);
166
+ } catch {
167
+ return {
168
+ ok: false,
169
+ reason: `There is no ${found.relativePath} in the project to start: export the build first, with project_export run.`
170
+ };
171
+ }
172
+ if (!isWithinRoot(realAsFarAsItExists(projectPath), real)) {
173
+ return {
174
+ ok: false,
175
+ reason: `${found.relativePath} leads outside the project, to ${real}, so it is not started.`
176
+ };
177
+ }
178
+ if (!statSync(real).isFile()) {
179
+ return { ok: false, reason: `${found.relativePath} is not a file, so there is nothing to start.` };
180
+ }
181
+ return { ok: true, absolutePath: real, relativePath: found.relativePath };
182
+ }
183
+ var RESOURCE_SCHEME = "res://", OTHER_SCHEME, KEPT_ID;
184
+ var init_paths = __esm(() => {
185
+ OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
186
+ KEPT_ID = /^[A-Za-z0-9_]+$/;
187
+ });
188
+
71
189
  // src/spawn.ts
72
190
  import {
73
191
  execFile,
@@ -243,7 +361,7 @@ var init_windows_ask = __esm(() => {
243
361
 
244
362
  // src/process-children.ts
245
363
  import { readFileSync } from "node:fs";
246
- import { basename } from "node:path";
364
+ import { basename as basename2 } from "node:path";
247
365
  async function startTimesOf(pids) {
248
366
  const began = new Map;
249
367
  if (pids.length === 0) {
@@ -345,7 +463,7 @@ function listingFailure(error) {
345
463
  const failed = error;
346
464
  const how = failed.killed === true ? `was killed after the ${ASK_TIMEOUT_MS}ms budget${failed.signal ? ` (${failed.signal})` : ""}` : failed.signal ? `was ended by ${failed.signal}` : failed.code !== undefined && failed.code !== null ? `exited with ${failed.code}` : "failed";
347
465
  const said = typeof failed.stderr === "string" ? failed.stderr.trim() : "";
348
- const asked = basename((failed.cmd ?? "").split(" ")[0] ?? "") || "the listing";
466
+ const asked = basename2((failed.cmd ?? "").split(" ")[0] ?? "") || "the listing";
349
467
  return `${asked} ${how}${said === "" ? ", with nothing on stderr" : `; stderr: ${said.slice(0, 400)}`}`;
350
468
  }
351
469
  function whyTheProcessTreeFailed() {
@@ -440,11 +558,21 @@ function ancestorsIn(tree, pid, root) {
440
558
  }
441
559
  return chain;
442
560
  }
561
+ function editorsOfProjectIn(tree, projectPath) {
562
+ const found = [];
563
+ for (const [pid, listed] of tree) {
564
+ const read = readCommandLine(listed.command);
565
+ if (read.editor && read.oneShot === null && read.projectPath !== null && /godot/i.test(read.executable) && !CONSOLE_WRAPPER.test(read.executable) && isSameDirectory(read.projectPath, projectPath)) {
566
+ found.push(pid);
567
+ }
568
+ }
569
+ return found.sort((a, b) => a - b);
570
+ }
443
571
  function readCommandLine(command) {
444
572
  const all = commandWords(command);
445
573
  const separator = all.findIndex((word) => word === "--" || word === "++");
446
574
  const words = separator === -1 ? all : all.slice(0, separator);
447
- const executable = words[0] === undefined ? "" : basename(words[0].replaceAll("\\", "/"));
575
+ const executable = words[0] === undefined ? "" : basename2(words[0].replaceAll("\\", "/"));
448
576
  const at = words.indexOf("--path");
449
577
  let projectPath = null;
450
578
  if (at !== -1 && at + 1 < words.length) {
@@ -507,8 +635,9 @@ async function askPosix() {
507
635
  });
508
636
  return stdout;
509
637
  }
510
- var ticksAnswered, ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS;
638
+ var ticksAnswered, ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS, CONSOLE_WRAPPER;
511
639
  var init_process_children = __esm(() => {
640
+ init_paths();
512
641
  init_spawn();
513
642
  init_windows_ask();
514
643
  ONE_SHOT_OPTIONS = [
@@ -532,13 +661,14 @@ var init_process_children = __esm(() => {
532
661
  "--convert-3to4",
533
662
  "--validate-conversion-3to4"
534
663
  ];
664
+ CONSOLE_WRAPPER = /_console(\.exe)?$/i;
535
665
  });
536
666
 
537
667
  // src/bridge-announce.ts
538
668
  import { mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
539
- import { dirname, join } from "node:path";
669
+ import { dirname as dirname2, join as join2 } from "node:path";
540
670
  function announcementPath(projectPath) {
541
- return join(projectPath, ".godot", "gdharness-bridge.json");
671
+ return join2(projectPath, ".godot", "gdharness-bridge.json");
542
672
  }
543
673
  function readAnnouncement(path) {
544
674
  try {
@@ -585,7 +715,7 @@ function announcementIsOurs(path) {
585
715
  function announceBridge(projectPath, bridge) {
586
716
  const path = announcementPath(projectPath);
587
717
  try {
588
- mkdirSync(dirname(path), { recursive: true });
718
+ mkdirSync(dirname2(path), { recursive: true });
589
719
  const announcement = {
590
720
  protocol: BRIDGE_ANNOUNCE_PROTOCOL,
591
721
  host: bridge.host,
@@ -652,15 +782,15 @@ import {
652
782
  openSync,
653
783
  readFileSync as readFileSync3,
654
784
  rmSync as rmSync2,
655
- statSync,
785
+ statSync as statSync2,
656
786
  writeFileSync as writeFileSync2,
657
787
  writeSync
658
788
  } from "node:fs";
659
789
  import { tmpdir as tmpdir2 } from "node:os";
660
- import { join as join2 } from "node:path";
790
+ import { join as join3 } from "node:path";
661
791
  import { setTimeout as delay } from "node:timers/promises";
662
792
  function helperAssembly(directory = tmpdir2()) {
663
- return join2(directory, `gdharness-desktop-${DIGEST}.dll`);
793
+ return join3(directory, `gdharness-desktop-${DIGEST}.dll`);
664
794
  }
665
795
  async function ensureHelperCompiled(directory = tmpdir2(), budgetMs = HELPER_COMPILE_MS, wait = true) {
666
796
  const assembly = helperAssembly(directory);
@@ -686,7 +816,7 @@ async function ensureHelperCompiled(directory = tmpdir2(), budgetMs = HELPER_COM
686
816
  }
687
817
  try {
688
818
  const holder = Number(readFileSync3(lock, "utf8").trim());
689
- const old = Date.now() - statSync(lock).mtimeMs > budgetMs;
819
+ const old = Date.now() - statSync2(lock).mtimeMs > budgetMs;
690
820
  if (old || Number.isInteger(holder) && holder > 0 && !alive(holder)) {
691
821
  rmSync2(lock, { force: true });
692
822
  continue;
@@ -700,8 +830,8 @@ async function ensureHelperCompiled(directory = tmpdir2(), budgetMs = HELPER_COM
700
830
  return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
701
831
  }
702
832
  async function compileHelper(directory, assembly, budgetMs) {
703
- const scratch = mkdtempSync(join2(directory, "gdharness-desktop-source-"));
704
- const source = join2(scratch, "helper.cs");
833
+ const scratch = mkdtempSync(join3(directory, "gdharness-desktop-source-"));
834
+ const source = join3(scratch, "helper.cs");
705
835
  writeFileSync2(source, SOURCE, "utf8");
706
836
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
707
837
  const script = [
@@ -8135,124 +8265,6 @@ var init_config_formats = __esm(() => {
8135
8265
  init_dist();
8136
8266
  });
8137
8267
 
8138
- // src/paths.ts
8139
- import { realpathSync, statSync as statSync2 } from "node:fs";
8140
- import { basename as basename2, dirname as dirname2, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
8141
- function realAsFarAsItExists(path) {
8142
- const missing = [];
8143
- let at = path;
8144
- for (;; ) {
8145
- try {
8146
- return join3(realpathSync.native(at), ...missing);
8147
- } catch {
8148
- const parent = dirname2(at);
8149
- if (parent === at) {
8150
- return path;
8151
- }
8152
- missing.unshift(basename2(at));
8153
- at = parent;
8154
- }
8155
- }
8156
- }
8157
- function isSameDirectory(onePath, otherPath) {
8158
- return relative(realAsFarAsItExists(resolve(onePath)), realAsFarAsItExists(resolve(otherPath))) === "";
8159
- }
8160
- function isWithinRoot(rootPath, candidatePath) {
8161
- const step = relative(resolve(rootPath), resolve(candidatePath));
8162
- return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
8163
- }
8164
- function keptInside(given) {
8165
- const at = given.indexOf("::");
8166
- const inner = at === -1 ? "" : given.slice(at + 2);
8167
- return KEPT_ID.test(inner) ? { file: given.slice(0, at), inner } : { file: given, inner: undefined };
8168
- }
8169
- function resolveWithinProject(projectPath, candidatePath, options = {}) {
8170
- const trimmed = candidatePath.trim();
8171
- if (trimmed === "") {
8172
- return { ok: false, reason: "Path is empty." };
8173
- }
8174
- if (trimmed.includes("\x00")) {
8175
- return { ok: false, reason: "Path contains a null byte." };
8176
- }
8177
- const root = resolve(projectPath);
8178
- const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : isAbsolute(trimmed) && isWithinRoot(root, trimmed) ? relative(root, resolve(trimmed)).split(sep).join("/") : trimmed;
8179
- if (withoutScheme.startsWith("uid://")) {
8180
- return {
8181
- ok: false,
8182
- reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
8183
- };
8184
- }
8185
- const scheme = OTHER_SCHEME.exec(withoutScheme);
8186
- if (scheme) {
8187
- const elsewhere = scheme[0].toLowerCase() === "user://" ? " A game's user:// is a directory of its own outside the project: a running game's is under userDir in editor_status runtimes and in editor_run start's runtime, where a file reader of your own reads it." : "";
8188
- return {
8189
- ok: false,
8190
- reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.${elsewhere}`
8191
- };
8192
- }
8193
- if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
8194
- const tail = withoutScheme.split(/[\\/]+/).filter((step) => step !== "" && !step.endsWith(":"));
8195
- const besideIt = isAbsolute(trimmed) && isWithinRoot(realAsFarAsItExists(dirname2(resolve(trimmed))), realAsFarAsItExists(root));
8196
- const example = tail.slice(besideIt ? -1 : -2).join("/") || "scenes/main.tscn";
8197
- return {
8198
- ok: false,
8199
- reason: `Path '${trimmed}' is absolute and outside the project; give a path inside it, such as '${example}'.`
8200
- };
8201
- }
8202
- const kept = keptInside(trimmed);
8203
- if (kept.inner !== undefined) {
8204
- return {
8205
- ok: false,
8206
- reason: `Path '${trimmed}' names the resource ${kept.inner} kept inside ${kept.file}, not a file. resource_edit read reads one by that path; this argument takes a file.`
8207
- };
8208
- }
8209
- if (withoutScheme.includes(":")) {
8210
- return {
8211
- ok: false,
8212
- reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
8213
- };
8214
- }
8215
- const absolutePath = resolve(root, withoutScheme);
8216
- if (!isWithinRoot(root, absolutePath)) {
8217
- return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
8218
- }
8219
- const relativePath = relative(root, absolutePath).split(sep).join("/");
8220
- if (relativePath === "" && options.root !== true) {
8221
- return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
8222
- }
8223
- return { ok: true, absolutePath, relativePath };
8224
- }
8225
- function exportedBuild(projectPath, candidatePath) {
8226
- const found = resolveWithinProject(projectPath, candidatePath);
8227
- if (!found.ok) {
8228
- return found;
8229
- }
8230
- let real;
8231
- try {
8232
- real = realpathSync.native(found.absolutePath);
8233
- } catch {
8234
- return {
8235
- ok: false,
8236
- reason: `There is no ${found.relativePath} in the project to start: export the build first, with project_export run.`
8237
- };
8238
- }
8239
- if (!isWithinRoot(realAsFarAsItExists(projectPath), real)) {
8240
- return {
8241
- ok: false,
8242
- reason: `${found.relativePath} leads outside the project, to ${real}, so it is not started.`
8243
- };
8244
- }
8245
- if (!statSync2(real).isFile()) {
8246
- return { ok: false, reason: `${found.relativePath} is not a file, so there is nothing to start.` };
8247
- }
8248
- return { ok: true, absolutePath: real, relativePath: found.relativePath };
8249
- }
8250
- var RESOURCE_SCHEME = "res://", OTHER_SCHEME, KEPT_ID;
8251
- var init_paths = __esm(() => {
8252
- OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
8253
- KEPT_ID = /^[A-Za-z0-9_]+$/;
8254
- });
8255
-
8256
8268
  // src/runner.ts
8257
8269
  import { existsSync as existsSync2 } from "node:fs";
8258
8270
  import { dirname as dirname3, join as join4 } from "node:path";
@@ -9905,6 +9917,29 @@ function cachedClasses(projectPath) {
9905
9917
  }
9906
9918
  return listed;
9907
9919
  }
9920
+ function cachedEntries(projectPath) {
9921
+ const text = readWhole(join6(projectPath, ".godot", "global_script_class_cache.cfg"));
9922
+ if (text === null) {
9923
+ return null;
9924
+ }
9925
+ const listed = new Map;
9926
+ for (const entry of text.matchAll(/\{[^{}]*\}/g)) {
9927
+ const named = /"class":\s*&"([^"]+)"/.exec(entry[0])?.[1];
9928
+ if (named !== undefined) {
9929
+ listed.set(named, entry[0]);
9930
+ }
9931
+ }
9932
+ return listed;
9933
+ }
9934
+ function cacheChangesBetween(before, after) {
9935
+ if (before === null || after === null) {
9936
+ return {};
9937
+ }
9938
+ const added = [...after.keys()].filter((name) => !before.has(name)).sort();
9939
+ const removed = [...before.keys()].filter((name) => !after.has(name)).sort();
9940
+ const changed = [...after.keys()].filter((name) => before.has(name) && before.get(name) !== after.get(name)).sort();
9941
+ return { added, removed, changed };
9942
+ }
9908
9943
  function addonOf(path) {
9909
9944
  return /^res:\/\/addons\/([^/]+)\//.exec(path)?.[1];
9910
9945
  }
@@ -9921,6 +9956,20 @@ function withAddonPathsCounted(paths) {
9921
9956
  }
9922
9957
  return { own, inAddons };
9923
9958
  }
9959
+ function classesWithAddonsCounted(classes, listed) {
9960
+ const kept = [];
9961
+ const inAddons = {};
9962
+ for (const entry of classes) {
9963
+ const path = entry !== null && typeof entry === "object" ? entry["path"] : "";
9964
+ const addon = addonOf(typeof path === "string" ? path : "");
9965
+ if (addon === undefined || listed.has(addon)) {
9966
+ kept.push(entry);
9967
+ } else {
9968
+ inAddons[addon] = (inAddons[addon] ?? 0) + 1;
9969
+ }
9970
+ }
9971
+ return { classes: kept, inAddons };
9972
+ }
9924
9973
  function withAddonClassesCounted(rebuilt, listed) {
9925
9974
  const counted = { ...rebuilt };
9926
9975
  for (const key of ["added", "changed"]) {
@@ -10978,9 +11027,10 @@ function rowOf(name) {
10978
11027
  function rowRuns(share, row) {
10979
11028
  return "only" in share ? row !== null && share.only.includes(row) : row === null || !share.besides.includes(row);
10980
11029
  }
10981
- function mismatchedRowsIn(printed) {
11030
+ function skippedAsDeclaredIn(printed) {
10982
11031
  const lines = printed.flatMap((text) => stripVTControlCharacters(text).split(/\r?\n/));
10983
11032
  const found = [];
11033
+ const unrunnable = [];
10984
11034
  let suite = null;
10985
11035
  let lastCase = null;
10986
11036
  for (let at = 0;at < lines.length; at += 1) {
@@ -10996,7 +11046,15 @@ function mismatchedRowsIn(printed) {
10996
11046
  lastCase = caseLine[3] === "STARTED" ? lastCase : caseLine[2] ?? null;
10997
11047
  continue;
10998
11048
  }
10999
- if (suite === null || lastCase === null || !MISMATCHED_ROW.test(line)) {
11049
+ if (suite === null || lastCase === null) {
11050
+ continue;
11051
+ }
11052
+ const declared = UNRUNNABLE_CASE.exec(line)?.[1];
11053
+ if (declared !== undefined) {
11054
+ unrunnable.push({ suite, name: lastCase, reason: declared });
11055
+ continue;
11056
+ }
11057
+ if (!MISMATCHED_ROW.test(line)) {
11000
11058
  continue;
11001
11059
  }
11002
11060
  const parts = [line.trim()];
@@ -11006,7 +11064,7 @@ function mismatchedRowsIn(printed) {
11006
11064
  }
11007
11065
  found.push({ suite, name: lastCase, reason: parts.join(" ").replaceAll(/\[\/?color[^\]]*\]/g, "") });
11008
11066
  }
11009
- return found;
11067
+ return { mismatched: found, unrunnable };
11010
11068
  }
11011
11069
  function readRun(printed) {
11012
11070
  const lines = stripVTControlCharacters(printed).replaceAll(`\r
@@ -11140,7 +11198,7 @@ function readRun(printed) {
11140
11198
  leftOut,
11141
11199
  engineVersion,
11142
11200
  orphansLeft,
11143
- mismatched: mismatchedRowsIn(lines)
11201
+ ...skippedAsDeclaredIn(lines)
11144
11202
  };
11145
11203
  }
11146
11204
  function withNestedErrors(printed, piped) {
@@ -11265,7 +11323,7 @@ function judgeEngine(printed, handed, end) {
11265
11323
  if (printed.executed === null || printed.executed === 0) {
11266
11324
  faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
11267
11325
  }
11268
- for (const row of printed.mismatched) {
11326
+ for (const row of [...printed.mismatched, ...printed.unrunnable]) {
11269
11327
  faults.push(`${row.suite} > ${row.name} never ran: gdUnit4 skipped it, saying "${row.reason}"`);
11270
11328
  failing.add(row.suite);
11271
11329
  }
@@ -11432,7 +11490,7 @@ ${String(error.warning)}`);
11432
11490
  const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text, warning: debt.warning })));
11433
11491
  return { printed: unexpected, neverPrinted };
11434
11492
  }
11435
- var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, MISMATCHED_ROW, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_ORPHANS = 101, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
11493
+ var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, MISMATCHED_ROW, UNRUNNABLE_CASE, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_ORPHANS = 101, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
11436
11494
  var init_suite_judge = __esm(() => {
11437
11495
  init_engine_leaks();
11438
11496
  init_junit();
@@ -11444,6 +11502,7 @@ var init_suite_judge = __esm(() => {
11444
11502
  WARNING_LINE = /^\s*(?:SCRIPT |USER )?WARNING: (.*?)\s*$/;
11445
11503
  EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
11446
11504
  MISMATCHED_ROW = /^\s*The test data set at index \(\d+\) does not match the expected test parameters/;
11505
+ UNRUNNABLE_CASE = /^\s*Reason: '((?:Unknown test case argument's \[.*\] found|Fuzzer argument '.+' must be declared before the test config argument\(s\) \[.*\])\.)'\s*$/;
11447
11506
  BANNER = /^\s*Godot Engine v(\S+?)(?:\s+-\s+\S+)?\s*$/;
11448
11507
  LEFT_OUT = /^gdharness left out (res:\/\/.+?\.gd) > ([^:\s]+):(-?\d+)\s*$/;
11449
11508
  DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
@@ -14231,7 +14290,8 @@ async function operationOutcome(engine, operation, params, projectPath) {
14231
14290
  ...afterAnswered
14232
14291
  };
14233
14292
  }
14234
- const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`;
14293
+ const onTheWayOut = answered === -1 ? problems : after;
14294
+ const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). ${onTheWayOut.length === 0 ? "It printed no error or warning on the way out." : `What it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`}`;
14235
14295
  return {
14236
14296
  ok: true,
14237
14297
  payload: answer.payload,
@@ -14794,7 +14854,7 @@ var init_ports = __esm(() => {
14794
14854
  import { existsSync as existsSync16, realpathSync as realpathSync2 } from "node:fs";
14795
14855
  import { readFile, realpath } from "node:fs/promises";
14796
14856
  import { createConnection } from "node:net";
14797
- import { dirname as dirname8, join as join20, resolve as resolve6 } from "node:path";
14857
+ import { dirname as dirname8, extname as extname2, join as join20, resolve as resolve6 } from "node:path";
14798
14858
  import { setTimeout as delay4 } from "node:timers/promises";
14799
14859
  import { fileURLToPath, pathToFileURL } from "node:url";
14800
14860
  function initializeTimeoutMs() {
@@ -15485,6 +15545,11 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
15485
15545
  } catch {
15486
15546
  throw new ArgumentRefusal(`Script file does not exist: res://${contained.relativePath}`);
15487
15547
  }
15548
+ if (!scriptPath.toLowerCase().endsWith(".gd")) {
15549
+ const said = `res://${contained.relativePath} is not a GDScript file, and the language server reads .gd files only`;
15550
+ const kind = extname2(scriptPath).toLowerCase();
15551
+ throw new ArgumentRefusal(kind === ".tscn" || kind === ".scn" ? `${said}: scene_tree reads a scene's nodes, and with properties the script each carries.` : kind === ".tres" || kind === ".res" ? `${said}: resource_edit read reads a resource.` : `${said}.`);
15552
+ }
15488
15553
  if (!isWithinRoot(projectPath, scriptPath)) {
15489
15554
  throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
15490
15555
  }
@@ -15633,7 +15698,7 @@ var init_lsp_client = __esm(() => {
15633
15698
 
15634
15699
  // src/resources.ts
15635
15700
  import { readFileSync as readFileSync17, realpathSync as realpathSync3 } from "node:fs";
15636
- import { extname as extname2, resolve as resolve8 } from "node:path";
15701
+ import { extname as extname3, resolve as resolve8 } from "node:path";
15637
15702
  function ensureProjectPath(getProjectPath) {
15638
15703
  const projectPath = getProjectPath();
15639
15704
  if (!projectPath) {
@@ -15692,7 +15757,7 @@ function parseGodotUri(uri) {
15692
15757
  }
15693
15758
  function ensureAllowedExtension(kind, filePath) {
15694
15759
  const allowed = EXTENSIONS[kind];
15695
- if (!allowed.includes(extname2(filePath).toLowerCase())) {
15760
+ if (!allowed.includes(extname3(filePath).toLowerCase())) {
15696
15761
  throw new Refusal(`godot://${kind}/ reads only ${allowed.join(", ")} files.`);
15697
15762
  }
15698
15763
  }
@@ -16040,6 +16105,25 @@ function shippedRuntimeDigest() {
16040
16105
  function installedEditorDigest(projectPath) {
16041
16106
  return digestMarker(projectPath, ADDONS[0]);
16042
16107
  }
16108
+ function editorUpgradeNote(sameCode, editors) {
16109
+ if (sameCode) {
16110
+ return ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
16111
+ ` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
16112
+ ` + " if the editor is running other code.";
16113
+ }
16114
+ if (editors === null) {
16115
+ return ` 1. An editor of this project that is open is still running the addons it loaded at
16116
+ ` + ` startup. Restart it with the editor_launch restart tool, which closes and reopens
16117
+ ` + " the window.";
16118
+ }
16119
+ if (editors.length === 0) {
16120
+ return ` 1. Nothing for the editor: none of this project is running, and the next one opened loads
16121
+ these addons.`;
16122
+ }
16123
+ const which = editors.length === 1 ? `The open editor (pid ${editors[0]}) is` : `The open editors (pids ${editors.join(", ")}) are`;
16124
+ return ` 1. ${which} still running the addons loaded at startup. Restart with the
16125
+ ` + " editor_launch restart tool, which closes and reopens the window.";
16126
+ }
16043
16127
  function installedRuntimeDigest(projectPath) {
16044
16128
  return digestMarker(projectPath, RUNTIME_ADDON);
16045
16129
  }
@@ -16511,6 +16595,11 @@ var init_tool_definitions = __esm(() => {
16511
16595
  },
16512
16596
  nodePath: { type: "string", description: "Target node, relative to the AnimationPlayer root." },
16513
16597
  property: { type: "string", description: "For property tracks: the property to animate." },
16598
+ update: {
16599
+ type: "string",
16600
+ enum: ["continuous", "discrete", "capture"],
16601
+ description: "For property tracks: how the value moves between keys. continuous blends toward the next key, so a bool or an int changes halfway to it; discrete holds each key's value until the next; capture blends from the value the property holds when playing starts. Without it a new track is made as the editor makes one: as the RESET animation's track for the same property, otherwise continuous for a float, vector, colour or transform and discrete for anything else. The answer gives the track's update."
16602
+ },
16514
16603
  method: {
16515
16604
  type: "string",
16516
16605
  description: "For method tracks: the method to call. A method track holds every method key for its node, so a key calling another method joins the node's track, with existingTrack true. The answer's methodFound says whether the target node has it; a method it lacks is still added, while a nodePath that reaches no node is refused, since it may be written next, and until it exists each call the track makes fails. A key whose args the method cannot take (too few or too many, or an object where it takes a value or the reverse) is written too, and answers argumentsFit false with misfit saying why. remove_track: only the keys calling this method."
@@ -16651,7 +16740,12 @@ var init_tool_definitions = __esm(() => {
16651
16740
  include: {
16652
16741
  type: "array",
16653
16742
  items: { type: "string", enum: [...PROJECT_INFO_SECTIONS] },
16654
- description: "Extra sections: registered autoloads, the classes the project's scripts declare with class_name, each with its script, read from the files, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets (runnable marks the one preset of its platform the editor's one-click deploy uses), the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, each file a scene, resource or script refers to that does not exist, each input action a script reads by a name written in the call that neither the project nor the engine defines, with every line reading it, and each signal connection in a scene whose method or signal the node at its end does not have, judged from the node's type and scripts as their files are written."
16743
+ description: "Extra sections: registered autoloads, the classes the project's scripts declare with class_name, each with its script, read from the files (those an addon declares counted per addon under inAddons, unless addonClasses names the addon), the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets (runnable marks the one preset of its platform the editor's one-click deploy uses), the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, each file a scene, resource or script refers to that does not exist, each input action a script reads by a name written in the call that neither the project nor the engine defines, with every line reading it, and each signal connection in a scene whose method or signal the node at its end does not have, judged from the node's type and scripts as their files are written."
16744
+ },
16745
+ addonClasses: {
16746
+ type: "array",
16747
+ items: { type: "string" },
16748
+ description: "For classes: the addons, by their folder under res://addons, whose classes are listed by name and script like the project's own, rather than counted under inAddons. Default none: a game with two large addons answered 686 classes, 19 of them its own."
16655
16749
  },
16656
16750
  preset: { type: "string", description: "For validation: the export preset to validate against." },
16657
16751
  detail: {
@@ -17046,7 +17140,7 @@ var init_tool_definitions = __esm(() => {
17046
17140
  path: {
17047
17141
  type: ["string", "array"],
17048
17142
  items: { type: "string" },
17049
- description: 'A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry "suite.gd:test_name" runs that case alone, as gdharness test reads it and as tests runs it, so it cannot share the list with a whole suite or a directory. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. A case declared there that gdUnit4 never ran, in a suite that passed, comes back under testsNotRun, and a row it skipped for data that does not fit the case under rowsMismatched with the reason gdUnit4 gave; neither run is called passed. Default tests and test, those the project has, which is where gdharness test looks.'
17143
+ description: 'A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry "suite.gd:test_name" runs that case alone, as gdharness test reads it and as tests runs it, so it cannot share the list with a whole suite or a directory. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. A case declared there that gdUnit4 never ran, in a suite that passed, comes back under testsNotRun, a row it skipped for data that does not fit the case under rowsMismatched, and a case it skipped because it cannot run it as declared (a parameter nothing fills, or a fuzzer after a setting) under casesUnrunnable, each with the reason gdUnit4 gave; none of those runs is called passed. Default tests and test, those the project has, which is where gdharness test looks.'
17050
17144
  },
17051
17145
  ignore: {
17052
17146
  type: "array",
@@ -17144,7 +17238,7 @@ var init_tool_definitions = __esm(() => {
17144
17238
  name: "scene_node",
17145
17239
  title: "Scene nodes",
17146
17240
  description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, paint TileMap cells, or bake a navigation region. Any ClassDB node type or script class can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties, and another scene is instanced by an add with instancePath. A set with properties {"name": "Boss"} renames a node, and answers nodePath as its new path. A rename or a reparent carries the node paths naming the node, as the editor's rename does: NodePath values on nodes (in arrays, dictionaries, built-in resources, %unique paths) and the tracks its players' animations hold, saving a library kept in a .tres of its own, and answers each rewrite under retargeted and each path it left under notRetargeted. The move is followed into every other .tscn that instances or inherits this scene at any depth: the editable children, values, added nodes and connections named by the old path are rewritten, since Godot finds a moved node's values by unique_id but editable children only by path and drops the values kept below them on the next save, and that scene's node paths and tracks are carried as above. A delete rewrites no path, and answers under stillNaming what named the deleted node and now names nothing. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene. A node this scene inherits takes values of this scene's own, and so does a node inside an instance of another scene once that instance's editableChildren is true; neither can be moved, renamed, copied or deleted, since the other scene makes it again on load. ${WITHOUT_AN_EDITOR}`,
17147
- answer: "get answers nodeType, the node's engine class, with scriptClass when its script declares a class_name; nodePath, its path from the root however it was named; setHere, the properties this scene's own file sets on it; and valuesFrom, the scene every other value comes from, when another scene makes the node. A set that gives a property the value the node has without it (what the scene it comes from gives, or its default) leaves it out of the file, as the editor does, and answers it under notStored, with notStoredNote saying what it follows now. retargeted lists each path a rename or a reparent rewrote, as {scenePath, nodePath, property or animation and track, from, to}, and notRetargeted each it left, with a reason: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. followed names the other scenes a move rewrote, and notFollowed any left as they were, with why. scriptsNaming lists each line of a script that reads a node a rename, a reparent or a delete took by the path it had, as {scenePath, nodePath, script, line, from, to}: the script is on the node at nodePath, or is one that node's script extends, in this scene or one instancing or inheriting it, and to is the path that reaches the node from there now, absent after a delete. It reads $Path, %Unique, and a string handed to get_node, get_node_or_null or has_node on the node itself; a path built at run time is not seen. Scripts are left as they were, as the editor's own rename leaves them, so each line listed fails until it is changed. A 2D bake_navigation that read no obstacle says in its note that all its outlines enclose is walkable, and names the nodes giving obstacles that sit under no region. stillNaming lists each node path and track, here and in the scenes instancing or inheriting this one, that named a deleted node, and deletedWithIt each node of this scene's own that went with it, the nodes inside an instance going with the instance. groups lists, sorted, every group a node is in, on a get, an add that gave addGroups and every set that changes them; get adds groupsHere, the groups this file puts it in, when another scene puts it in others, and layers, each layer mask it answers (collision_layer, collision_mask, visibility_layer) as the layers it holds, {layer, name} with the name project.godot gives the layer. Each of these lists is answered only when it names anything. add and set answer each property given under properties as the node holds it once written; alsoChanged gives the other properties the engine changed in step with a set, as a Control's offsets with its size, and those a reparent changed to keep the node where it is drawn, as {was, now}; namesNoNode, each node path written naming no node; sharedWith, for a property written through a resource built into the scene, each other {node, property} holding it, which changed too; setByScript, what a script that is not a tool set as a headless write ran, which the file does not hold; loadsAs, a value given that the file holds and the node's own script changes as the scene loads, as it loads. get adds placement, a Node3D's position, rotation_degrees and scale from its transform. " + ALSO_SAVED,
17241
+ answer: "get answers nodeType, the node's engine class, with scriptClass when its script declares a class_name; nodePath, its path from the root however it was named; setHere, the properties this scene's own file sets on it; and valuesFrom, the scene every other value comes from, when another scene makes the node. A set that gives a property the value the node has without it (what the scene it comes from gives, or its default) leaves it out of the file, as the editor does, and answers it under notStored, with notStoredNote saying what it follows now. retargeted lists each path a rename or a reparent rewrote, as {scenePath, nodePath, property or animation and track, from, to}, and notRetargeted each it left, with a reason: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. followed names the other scenes a move rewrote, and notFollowed any left as they were, with why. scriptsNaming lists each line of a script that reads a node a rename, a reparent or a delete took by the path it had, as {scenePath, nodePath, script, line, from, to}: the script is on the node at nodePath, or is one that node's script extends, in this scene or one instancing or inheriting it, and to is the path that reaches the node from there now, absent after a delete. It reads $Path, %Unique, and a string handed to get_node, get_node_or_null or has_node on the node itself; a path built at run time is not seen. Scripts are left as they were, as the editor's own rename leaves them, so each line listed fails until it is changed. A 2D bake_navigation that read no obstacle says in its note that all its outlines enclose is walkable, and names the nodes giving obstacles that sit under no region. stillNaming lists each node path and track, here and in the scenes instancing or inheriting this one, that named a deleted node; a property typed as a node, or an array or dictionary of nodes, holds the node itself, so the one that held a deleted node holds null there, as the editor saves it, and its entry adds cleared: true. deletedWithIt lists each node of this scene's own that went with it, the nodes inside an instance going with the instance. groups lists, sorted, every group a node is in, on a get, an add that gave addGroups and every set that changes them; get adds groupsHere, the groups this file puts it in, when another scene puts it in others, and layers, each layer mask it answers (collision_layer, collision_mask, visibility_layer) as the layers it holds, {layer, name} with the name project.godot gives the layer. Each of these lists is answered only when it names anything. add and set answer each property given under properties as the node holds it once written; alsoChanged gives the other properties the engine changed in step with a set, as a Control's offsets with its size, and those a reparent changed to keep the node where it is drawn or that the new parent sets, as a container gives a Control its layout_mode, as {was, now}; namesNoNode, each node path written naming no node; sharedWith, for a property written through a resource built into the scene, each other {node, property} holding it, which changed too; setByScript, what a script that is not a tool set as a headless write ran, which the file does not hold; loadsAs, a value given that the file holds and the node's own script changes as the scene loads, as it loads. get adds placement, a Node3D's position, rotation_degrees and scale from its transform. " + ALSO_SAVED,
17148
17242
  parameters: {
17149
17243
  projectPath: PROJECT_PATH,
17150
17244
  scenePath: SCENE_PATH,
@@ -17186,7 +17280,11 @@ var init_tool_definitions = __esm(() => {
17186
17280
  ops: ["set"],
17187
17281
  description: "set: groups to take the node out of. One it is not in is refused, and so is one another scene puts it in (the scene that makes it, instances it or is inherited here), which only that scene can take it out of."
17188
17282
  },
17189
- newName: { type: "string", ops: ["duplicate"], description: "duplicate: the copy's name." },
17283
+ newName: {
17284
+ type: "string",
17285
+ ops: ["duplicate"],
17286
+ description: "duplicate: the copy's name. The copy has the nodes under the original and their connections, as the editor's Duplicate makes it: each connection keeps its flags, binds and unbinds, and one between two copied nodes joins the copies."
17287
+ },
17190
17288
  newParentPath: {
17191
17289
  type: "string",
17192
17290
  ops: ["reparent"],
@@ -17258,7 +17356,7 @@ var init_tool_definitions = __esm(() => {
17258
17356
  {
17259
17357
  name: "scene_signal",
17260
17358
  title: "Scene signals",
17261
- description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connection carries binds, the values passed after the signal's own, and unbinds, how many of the signal's own are dropped first, as the editor's connect dialog offers them; each is answered only when there is one. A connect of a pair already connected says alreadyConnected, with flagsBefore, bindsBefore and unbindsBefore when it changed them, and keeps the binds and unbinds it had unless the call gives new ones. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. One whose method cannot take what the signal passes, with its binds and unbinds (too few or too many arguments, an object where the method takes a value or the reverse, or an engine class unrelated to the one it takes), is written too, and answers argumentsFit false with misfit saying why; list gives each connection its methodFound, and misfit when it has one. list marks editable false on a connection a scene this one instances or inherits makes, on its own root as well as inside it, and names that scene under madeIn; only that scene can change it, so a disconnect of one is refused. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
17359
+ description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connection carries binds, the values passed after the signal's own, and unbinds, how many of the signal's own are dropped first, as the editor's connect dialog offers them; each is answered only when there is one. A connect of a pair already connected says alreadyConnected, with flagsBefore, bindsBefore and unbindsBefore when it changed them, and keeps the binds and unbinds it had unless the call gives new ones. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. That holds only without binds and unbinds: the engine makes a connection that binds or unbinds arguments only to a method its target has, and a scene loads without one aimed elsewhere, so such a connect is refused. A file can hold one already, written by hand or before its method was renamed: the engine loads the scene without it and a save drops it, so every tool that writes a .tscn puts it back as the file held it and says so under unmadeConnections, a write to a binary .scn holding one is refused, and a disconnect takes it out. One whose method cannot take what the signal passes, with its binds and unbinds (too few or too many arguments, an object where the method takes a value or the reverse, or an engine class unrelated to the one it takes), is written too, and answers argumentsFit false with misfit saying why; list gives each connection its methodFound, and misfit when it has one. list marks editable false on a connection a scene this one instances or inherits makes, on its own root as well as inside it, and names that scene under madeIn; only that scene can change it, so a disconnect of one is refused. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
17262
17360
  parameters: {
17263
17361
  projectPath: PROJECT_PATH,
17264
17362
  scenePath: SCENE_PATH,
@@ -18065,7 +18163,7 @@ var init_tool_definitions = __esm(() => {
18065
18163
  property: {
18066
18164
  type: "string",
18067
18165
  ops: ["property", "find"],
18068
- description: `property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A key holding a colon, a bracket or a quote is written in double quotes, 'stock:"base:dew"', as a refusal listing the keys writes it. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. It also answers the calls that read it with no arguments, by its type, such as "linear_velocity:length()" or "global_transform:get_origin()"; one it does not answer is refused, naming those it does. A string answers such calls too, "text:length()" or "text:strip_edges()", and "text:to_utf32_buffer():to_int32_array()" gives its code points, which tell a non-breaking space (160) from the space an answer shows in its place. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control's theme is read where it is drawn: 'get_theme_stylebox("panel"):content_margin_top' or 'get_theme_constant("separation")'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.`
18166
+ description: `property: which one to read, or a constant the node's script or a script it extends declares. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A key holding a colon, a bracket or a quote is written in double quotes, 'stock:"base:dew"', as a refusal listing the keys writes it. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. It also answers the calls that read it with no arguments, by its type, such as "linear_velocity:length()" or "global_transform:get_origin()"; one it does not answer is refused, naming those it does. A string answers such calls too, "text:length()" or "text:strip_edges()", and "text:to_utf32_buffer():to_int32_array()" gives its code points, which tell a non-breaking space (160) from the space an answer shows in its place. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control's theme is read where it is drawn: 'get_theme_stylebox("panel"):content_margin_top' or 'get_theme_constant("separation")'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.`
18069
18167
  },
18070
18168
  depth: {
18071
18169
  type: "integer",
@@ -18532,7 +18630,7 @@ var init_tool_definitions = __esm(() => {
18532
18630
  says: {
18533
18631
  type: "string",
18534
18632
  ops: ["until"],
18535
- description: 'until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it, and alternatives separated by | met by whichever appears first ("*won*|*lost*"); on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took, and says under looking_ms how long the looks took between them.'
18633
+ description: 'until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it, and alternatives separated by | met by whichever appears first ("*won*|*lost*"); on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took, and says under looking_ms how long the looks took between them. A look on a large screen is read about 2ms to a frame, so no frame grows by more, except the last look as the time runs out, which is read whole; longest_hold_ms is the longest the looks held one frame, which tells a long frame the game logs apart from one the wait caused.'
18536
18634
  },
18537
18635
  includeHidden: {
18538
18636
  type: "boolean",
@@ -41762,6 +41860,30 @@ function cutTestRunNote(run) {
41762
41860
  }
41763
41861
  var init_cut_test_run = () => {};
41764
41862
 
41863
+ // src/godot-variant.ts
41864
+ var FLAG_64, MOST_MESSAGE_BYTES;
41865
+ var init_godot_variant = __esm(() => {
41866
+ FLAG_64 = 1 << 16;
41867
+ MOST_MESSAGE_BYTES = (8 << 20) + 4;
41868
+ });
41869
+
41870
+ // src/game-debugger.ts
41871
+ var PAUSE_MS = 30000, STEP_MS = 1e4, RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", RUNTIME_AUTOLOAD2, READ_MS = 5000, EVALUATE_MS = 60000, REPLIES;
41872
+ var init_game_debugger = __esm(() => {
41873
+ init_godot_variant();
41874
+ init_setup();
41875
+ RUNTIME_AUTOLOAD2 = `${RUNTIME_ADDON2}runtime_autoload.gd`;
41876
+ REPLIES = new Set([
41877
+ "debug_enter",
41878
+ "debug_exit",
41879
+ "stack_dump",
41880
+ "stack_frame_vars",
41881
+ "stack_frame_var",
41882
+ "evaluation_return",
41883
+ "scene:inspect_object"
41884
+ ]);
41885
+ });
41886
+
41765
41887
  // src/dap_client.ts
41766
41888
  import { createConnection as createConnection3 } from "node:net";
41767
41889
  import { setTimeout as delay6 } from "node:timers/promises";
@@ -41804,11 +41926,13 @@ class GodotDAPClient {
41804
41926
  onBreakpointsChanged = null;
41805
41927
  scopesPatienceMs;
41806
41928
  stepPatienceMs;
41929
+ evaluatePatienceMs;
41807
41930
  constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1", patience = {}) {
41808
41931
  this.port = port;
41809
41932
  this.host = host;
41810
41933
  this.scopesPatienceMs = patience.scopesMs ?? SCOPES_TIMEOUT_MS;
41811
41934
  this.stepPatienceMs = patience.stepMs ?? STEP_LANDS_WITHIN_MS;
41935
+ this.evaluatePatienceMs = patience.evaluateMs ?? EVALUATE_MS;
41812
41936
  this.pendingRequests = new Map;
41813
41937
  }
41814
41938
  async connect() {
@@ -42312,7 +42436,7 @@ class GodotDAPClient {
42312
42436
  if (members?.variables?.some((variable) => variable["name"] === "self") !== true) {
42313
42437
  throw new ReadRefusal(`Frame ${frame} has no instance of its script to evaluate on, as a static function called on its class has none, and the engine lets the game go on rather than answer one there, so it was not asked. Evaluate in a frame below it, or read this one's locals with debug_state variables`);
42314
42438
  }
42315
- const deadline = Date.now() + this.scopesPatienceMs;
42439
+ const deadline = Date.now() + this.evaluatePatienceMs;
42316
42440
  while (Date.now() < deadline) {
42317
42441
  try {
42318
42442
  const answered = await this.sendRequest("evaluate", { expression, frameId: frame, context: "watch" }, VARIABLES_POLL_TIMEOUT_MS);
@@ -42324,7 +42448,7 @@ class GodotDAPClient {
42324
42448
  } catch {}
42325
42449
  await delay6(200);
42326
42450
  }
42327
- throw new ReadRefusal(`What ${expression} comes to did not arrive from the game within ${this.scopesPatienceMs / 1000}s. It comes from the game through the editor: ask again`);
42451
+ throw new ReadRefusal(`What ${expression} comes to did not arrive from the game within ${this.evaluatePatienceMs / 1000}s. It comes from the game through the editor, and the first expression a game evaluates loads the script of every global class in the project first, static initialisers included, so one of those may still be running: ask again`);
42328
42452
  }
42329
42453
  isConnected() {
42330
42454
  return this.connected;
@@ -42471,6 +42595,7 @@ var init_dap_client = __esm(() => {
42471
42595
  init_answer_json();
42472
42596
  init_errors();
42473
42597
  init_framing();
42598
+ init_game_debugger();
42474
42599
  init_ports();
42475
42600
  ReadRefusal = class ReadRefusal extends Refusal {
42476
42601
  };
@@ -44040,7 +44165,7 @@ import {
44040
44165
  statSync as statSync17,
44041
44166
  writeFileSync as writeFileSync19
44042
44167
  } from "node:fs";
44043
- import { basename as basename10, dirname as dirname22, extname as extname3, join as join38, relative as relative8 } from "node:path";
44168
+ import { basename as basename10, dirname as dirname22, extname as extname4, join as join38, relative as relative8 } from "node:path";
44044
44169
  function filesUnder(directory) {
44045
44170
  return readdirSync17(directory, { withFileTypes: true }).flatMap((entry) => {
44046
44171
  const path = join38(directory, entry.name);
@@ -44160,10 +44285,10 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
44160
44285
  if (owner !== undefined && owner !== null) {
44161
44286
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
44162
44287
  }
44163
- if (extname3(from).toLowerCase() !== extname3(to).toLowerCase()) {
44288
+ if (extname4(from).toLowerCase() !== extname4(to).toLowerCase()) {
44164
44289
  return {
44165
44290
  ok: false,
44166
- reason: `${from} would become ${to}, and the engine picks how to read a file by its extension: give newPath the extension it has, ${extname3(from) || "none"}.`
44291
+ reason: `${from} would become ${to}, and the engine picks how to read a file by its extension: give newPath the extension it has, ${extname4(from) || "none"}.`
44167
44292
  };
44168
44293
  }
44169
44294
  }
@@ -44320,30 +44445,6 @@ var init_file_move = __esm(() => {
44320
44445
  SIDECARS = [".uid", ".import"];
44321
44446
  });
44322
44447
 
44323
- // src/godot-variant.ts
44324
- var FLAG_64, MOST_MESSAGE_BYTES;
44325
- var init_godot_variant = __esm(() => {
44326
- FLAG_64 = 1 << 16;
44327
- MOST_MESSAGE_BYTES = (8 << 20) + 4;
44328
- });
44329
-
44330
- // src/game-debugger.ts
44331
- var PAUSE_MS = 30000, STEP_MS = 1e4, RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", RUNTIME_AUTOLOAD2, READ_MS = 5000, REPLIES;
44332
- var init_game_debugger = __esm(() => {
44333
- init_godot_variant();
44334
- init_setup();
44335
- RUNTIME_AUTOLOAD2 = `${RUNTIME_ADDON2}runtime_autoload.gd`;
44336
- REPLIES = new Set([
44337
- "debug_enter",
44338
- "debug_exit",
44339
- "stack_dump",
44340
- "stack_frame_vars",
44341
- "stack_frame_var",
44342
- "evaluation_return",
44343
- "scene:inspect_object"
44344
- ]);
44345
- });
44346
-
44347
44448
  // node_modules/ws/lib/constants.js
44348
44449
  var require_constants = __commonJS(function(exports, module) {
44349
44450
  var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"];
@@ -49221,7 +49322,7 @@ import {
49221
49322
  statSync as statSync20,
49222
49323
  writeFileSync as writeFileSync22
49223
49324
  } from "node:fs";
49224
- import { basename as basename13, dirname as dirname25, extname as extname4, isAbsolute as isAbsolute8, join as join44, normalize as normalize2, relative as relative10 } from "node:path";
49325
+ import { basename as basename13, dirname as dirname25, extname as extname5, isAbsolute as isAbsolute8, join as join44, normalize as normalize2, relative as relative10 } from "node:path";
49225
49326
  import { setTimeout as delay7 } from "node:timers/promises";
49226
49327
  import { fileURLToPath as fileURLToPath5 } from "node:url";
49227
49328
  function editorRestartTimeoutMs() {
@@ -49273,7 +49374,7 @@ function writtenFileRefusal(path, rule) {
49273
49374
  if (!isAbsolute8(path)) {
49274
49375
  return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
49275
49376
  }
49276
- if (!rule.extensions.includes(extname4(path).toLowerCase())) {
49377
+ if (!rule.extensions.includes(extname5(path).toLowerCase())) {
49277
49378
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
49278
49379
  }
49279
49380
  const directory = realAsFarAsItExists(dirname25(path));
@@ -49305,7 +49406,7 @@ function movieRefusal(engineArgs, projects, headless) {
49305
49406
  extensions: [".avi", ".png"],
49306
49407
  projects,
49307
49408
  writes: (where) => {
49308
- if (extname4(where).toLowerCase() !== ".png") {
49409
+ if (extname5(where).toLowerCase() !== ".png") {
49309
49410
  return [where];
49310
49411
  }
49311
49412
  const base = where.slice(0, -".png".length);
@@ -49620,16 +49721,6 @@ function endedPreviousRun(ended) {
49620
49721
  function previousRunLeft(ended) {
49621
49722
  return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
49622
49723
  }
49623
- function editorsOfProjectIn(tree, projectPath) {
49624
- const found = [];
49625
- for (const [pid, listed] of tree) {
49626
- const read = readCommandLine(listed.command);
49627
- if (read.editor && read.oneShot === null && read.projectPath !== null && /godot/i.test(read.executable) && !CONSOLE_WRAPPER.test(read.executable) && isSameDirectory(read.projectPath, projectPath)) {
49628
- found.push(pid);
49629
- }
49630
- }
49631
- return found.sort((a, b) => a - b);
49632
- }
49633
49724
  function engineNamesOf(executable) {
49634
49725
  return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
49635
49726
  }
@@ -52643,6 +52734,9 @@ class GodotServer {
52643
52734
  if (forValidation.length > 0 && !include.includes("validation")) {
52644
52735
  return this.createErrorResponse(`${forValidation.join(" and ")} ${forValidation.length === 1 ? "is" : "are"} read only by the validation section, which this call does not include: add "validation" to include, or leave ${forValidation.length === 1 ? "it" : "them"} out.`);
52645
52736
  }
52737
+ if (args["addonClasses"] !== undefined && !include.includes("classes")) {
52738
+ return this.createErrorResponse('addonClasses is read only by the classes section, which this call does not include: add "classes" to include, or leave it out.');
52739
+ }
52646
52740
  const project = this.project(args);
52647
52741
  if (!project.ok) {
52648
52742
  return project.response;
@@ -52673,6 +52767,23 @@ class GodotServer {
52673
52767
  ...compiledScripts !== undefined && (section === "health" || section === "validation") ? { compiled: compiledScripts } : {}
52674
52768
  };
52675
52769
  const outcome = await this.operation(spec.operation, params, project.value.path);
52770
+ if (section === "classes" && outcome.ok && Array.isArray(outcome.payload["classes"])) {
52771
+ const declared = outcome.payload["classes"];
52772
+ const asked = readStringArray(args, "addonClasses") ?? [];
52773
+ const addons = Object.keys(classesWithAddonsCounted(declared, new Set).inAddons).sort();
52774
+ const unknownAddons = asked.filter((addon) => !addons.includes(addon));
52775
+ if (unknownAddons.length > 0) {
52776
+ return this.createErrorResponse(`addonClasses names ${unknownAddons.join(", ")}, and no folder of that name under res://addons declares a class. ${addons.length === 0 ? "No addon declares one." : `The addons declaring classes are ${addons.join(", ")}.`}`);
52777
+ }
52778
+ const counted = classesWithAddonsCounted(declared, new Set(asked));
52779
+ info[section] = {
52780
+ ...outcome.payload,
52781
+ classes: counted.classes,
52782
+ ...Object.keys(counted.inAddons).length > 0 ? { inAddons: counted.inAddons } : {},
52783
+ ...engineExtras(outcome)
52784
+ };
52785
+ continue;
52786
+ }
52676
52787
  info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
52677
52788
  }
52678
52789
  return this.jsonTextResponse(info);
@@ -52887,7 +52998,7 @@ ${platform}`;
52887
52998
  if (pack && debug) {
52888
52999
  return this.createErrorResponse("A pack is exported as a release: the command-line exporter has no debug pack, so debug and pack cannot both be true.", ["Leave debug out for a pack", "Or leave pack out for a debug build"]);
52889
53000
  }
52890
- const extension = extname4(output.relativePath).toLowerCase();
53001
+ const extension = extname5(output.relativePath).toLowerCase();
52891
53002
  if (pack && extension !== ".pck" && extension !== ".zip") {
52892
53003
  return this.createErrorResponse(`A pack is written as a .pck or a .zip, and ${output.relativePath} is neither: the exporter picks the format by the extension.`, ["Give outputPath ending in .pck, or .zip for a zip of the resources"]);
52893
53004
  }
@@ -53408,10 +53519,14 @@ ${error.text}`;
53408
53519
  test: `${row.suite} > ${row.name}`,
53409
53520
  reason: row.reason
53410
53521
  }));
53522
+ const casesUnrunnable = casePrinted.unrunnable.map((one) => ({
53523
+ test: `${one.suite} > ${one.name}`,
53524
+ reason: one.reason
53525
+ }));
53411
53526
  return this.jsonTextResponse({
53412
- passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && countedLeaks.length === 0 && missing.length === 0 && testsNotRun.length === 0 && casesNotRun.length === 0 && rowsMismatched.length === 0 && notSuites.length === 0,
53527
+ passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && countedLeaks.length === 0 && missing.length === 0 && testsNotRun.length === 0 && casesNotRun.length === 0 && rowsMismatched.length === 0 && casesUnrunnable.length === 0 && notSuites.length === 0,
53413
53528
  verdict: [
53414
- verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length + countedLeaks.length + testsNotRun.length + casesNotRun.length + rowsMismatched.length + notSuites.length > 0 ? undefined : verdict,
53529
+ verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length + countedLeaks.length + testsNotRun.length + casesNotRun.length + rowsMismatched.length + casesUnrunnable.length + notSuites.length > 0 ? undefined : verdict,
53415
53530
  ...[false, true].map((warning) => {
53416
53531
  const count = undeclaredPrinted.filter((error) => error.warning === warning).length;
53417
53532
  const word = warning ? "warning" : "error";
@@ -53425,12 +53540,14 @@ ${error.text}`;
53425
53540
  countedLeaks.length > 0 ? "the engine exited holding what it should have freed" : undefined,
53426
53541
  testsNotRun.length + casesNotRun.length > 0 ? `not run: ${[...testsNotRun, ...casesNotRun].join(", ")}` : undefined,
53427
53542
  rowsMismatched.length > 0 ? `${rowsMismatched.length === 1 ? "one row" : `${String(rowsMismatched.length)} rows`} skipped for data that does not fit the case` : undefined,
53543
+ casesUnrunnable.length > 0 ? `${casesUnrunnable.length === 1 ? "one case" : `${String(casesUnrunnable.length)} cases`} skipped as gdUnit4 cannot run ${casesUnrunnable.length === 1 ? "it" : "them"} as declared` : undefined,
53428
53544
  notSuites.length > 0 ? `${notSuites.length === 1 ? "one script declares" : `${String(notSuites.length)} scripts declare`} cases gdUnit4 never runs` : undefined
53429
53545
  ].filter((part) => part !== undefined).join("; "),
53430
53546
  ...missing.length > 0 ? { missing } : {},
53431
53547
  ...notSuites.length > 0 ? { notSuites } : {},
53432
53548
  ...testsNotRun.length + casesNotRun.length > 0 ? { testsNotRun: [...testsNotRun, ...casesNotRun] } : {},
53433
53549
  ...rowsMismatched.length > 0 ? { rowsMismatched } : {},
53550
+ ...casesUnrunnable.length > 0 ? { casesUnrunnable } : {},
53434
53551
  ...timedOut ? { timedOut, hung, silentForMs } : {},
53435
53552
  exitCode,
53436
53553
  exitSignal: run.exitSignal ?? undefined,
@@ -53464,6 +53581,7 @@ ${error.text}`;
53464
53581
  testsNotRun.length > 0 ? `${testsNotRun.join(", ")} ${testsNotRun.length === 1 ? "was" : "were"} asked for and not run, so the run is not called passed: gdUnit4 runs a test_ function only in a script that extends GdUnitTestSuite, and leaves out a suite whose script does not load.` : undefined,
53465
53582
  casesNotRun.length > 0 ? `${casesNotRun.join(", ")} ${casesNotRun.length === 1 ? "is a case" : "are cases"} of a suite that ran and passed, and gdUnit4 never ran ${casesNotRun.length === 1 ? "it" : "them"} and said nothing of it, so the run is not called passed, as gdharness test judges it.` : undefined,
53466
53583
  rowsMismatched.length > 0 ? `gdUnit4 skipped what rowsMismatched names because the row's data does not fit the case's parameters, though it printed ${rowsMismatched.length === 1 ? "the row" : "each row"} passed first; each entry carries gdUnit4's reason. The run is not called passed, as gdharness test judges it.` : undefined,
53584
+ casesUnrunnable.length > 0 ? `gdUnit4 skipped what casesUnrunnable names because it cannot run ${casesUnrunnable.length === 1 ? "that case" : "those cases"} as declared, though it printed ${casesUnrunnable.length === 1 ? "it" : "each"} passed first; each entry carries gdUnit4's reason. A parameter is filled only by _test_parameters, a fuzzer or a setting gdUnit4 names (timeout, do_skip, skip_reason, fuzzer_iterations, fuzzer_seed), and fuzzers come before the settings. The run is not called passed, as gdharness test judges it.` : undefined,
53467
53585
  notSuites.length > 0 ? "What notSuites names declares cases gdUnit4 never runs, so the run is not called passed, as gdharness test judges it." : undefined,
53468
53586
  briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
53469
53587
  notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
@@ -53998,7 +54116,7 @@ ${error.text}`;
53998
54116
  return { ...game, keeper: game.keeper };
53999
54117
  }
54000
54118
  async askTheKeptGame(keeper, request, projectPath) {
54001
- const answered = await askTheKeepersDebugger(keeper, request, KEEPER_DEBUG_MS);
54119
+ const answered = await askTheKeepersDebugger(keeper, request, request.op === "evaluate" ? EVALUATE_MS + 2 * READ_MS : KEEPER_DEBUG_MS);
54002
54120
  if ("unreached" in answered) {
54003
54121
  return this.createErrorResponse(`The debugger of the running game did not answer: ${String(answered.unreached)}.`, [
54004
54122
  "editor_run start starts the game again, with a debugger",
@@ -57522,6 +57640,7 @@ ${error.text}`;
57522
57640
  async settleRenameOnDisk(projectPath, changed, asked) {
57523
57641
  const report = {};
57524
57642
  let ok = true;
57643
+ const cacheBefore = asked.rebuildCache ? cachedEntries(projectPath) : null;
57525
57644
  if (asked.moved) {
57526
57645
  const engine = await this.engine();
57527
57646
  const sidecarsBefore = new Set(projectFiles(projectPath, [".uid", ".import"]));
@@ -57536,7 +57655,7 @@ ${error.text}`;
57536
57655
  }
57537
57656
  if (asked.rebuildCache) {
57538
57657
  const rebuilt = await this.rebuildClassCache(projectPath);
57539
- report["classCache"] = rebuilt.ok ? rebuilt.payload : { rebuilt: false, problem: rebuilt.message };
57658
+ report["classCache"] = rebuilt.ok ? { ...rebuilt.payload, ...cacheChangesBetween(cacheBefore, cachedEntries(projectPath)) } : { rebuilt: false, problem: rebuilt.message };
57540
57659
  ok &&= rebuilt.ok;
57541
57660
  }
57542
57661
  if (changed.length > 0) {
@@ -58102,7 +58221,7 @@ async function runGodotServer() {
58102
58221
  const server = new GodotServer;
58103
58222
  await server.run();
58104
58223
  }
58105
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
58224
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
58106
58225
  var init_server = __esm(() => {
58107
58226
  init_dist2();
58108
58227
  init_stdio();
@@ -58171,7 +58290,6 @@ var init_server = __esm(() => {
58171
58290
  init_update_check();
58172
58291
  __dirname2 = dirname25(fileURLToPath5(import.meta.url));
58173
58292
  KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
58174
- CONSOLE_WRAPPER = /_console(\.exe)?$/i;
58175
58293
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
58176
58294
  VALUE_NAMES = {
58177
58295
  string: "a string",
@@ -58461,6 +58579,7 @@ async function pipe(port) {
58461
58579
 
58462
58580
  // src/cli.ts
58463
58581
  init_paths();
58582
+ init_process_children();
58464
58583
 
58465
58584
  // src/prompt.ts
58466
58585
  import process12 from "node:process";
@@ -60317,10 +60436,8 @@ async function upgrade() {
60317
60436
  }
60318
60437
  console.log(`
60319
60438
  On ${version}. What is left for you:`);
60320
- console.log(digestBefore !== null && digestBefore === installedEditorDigest(projectPath) ? ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
60321
- ` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
60322
- ` + " if the editor is running other code." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
60323
- ` + " editor_launch restart tool, which closes and reopens the window.");
60439
+ const tree = await processTree();
60440
+ console.log(editorUpgradeNote(digestBefore !== null && digestBefore === installedEditorDigest(projectPath), tree === undefined ? null : editorsOfProjectIn(tree, projectPath)));
60324
60441
  const serving = await serverServing(projectPath);
60325
60442
  console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" "), serving === null ? null : { server: serving, isThisVersion: serving.version === version }));
60326
60443
  const trailing = worktreeCheckoutsOf(projectPath).flatMap((path) => {