gdharness 1.3.40 → 1.3.41

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
@@ -12910,7 +12910,7 @@ var init_doc_cache = __esm(() => {
12910
12910
 
12911
12911
  // src/project-scan.ts
12912
12912
  import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync9 } from "node:fs";
12913
- import { basename as basename5, extname, join as join11 } from "node:path";
12913
+ import { basename as basename5, extname, join as join11, relative as relative4, sep as sep3 } from "node:path";
12914
12914
  function skipped(name) {
12915
12915
  return name.startsWith(".") || SKIPPED.has(name);
12916
12916
  }
@@ -13070,6 +13070,20 @@ function pathsInUidCache(projectPath) {
13070
13070
  }
13071
13071
  return new Set(paths.values());
13072
13072
  }
13073
+ function steppedOverAround(projectPath, place) {
13074
+ let at = projectPath;
13075
+ for (const step of relative4(projectPath, place).split(sep3).filter((part) => part !== "")) {
13076
+ at = join11(at, step);
13077
+ if (!statSync9(at).isDirectory()) {
13078
+ break;
13079
+ }
13080
+ const why = skipped(step) ? step.startsWith(".") ? "its name starts with a dot" : `it is ${step}` : gdignored(at) ? "it holds a .gdignore" : installedByUs(at) ? "gdharness installed it" : null;
13081
+ if (why !== null) {
13082
+ return { folder: `res://${relative4(projectPath, at).split(sep3).join("/")}/`, why };
13083
+ }
13084
+ }
13085
+ return null;
13086
+ }
13073
13087
  function isBinary(bytes) {
13074
13088
  return bytes.subarray(0, 8000).includes(0);
13075
13089
  }
@@ -13195,8 +13209,8 @@ function searchProject(projectPath, options) {
13195
13209
  result.summary.files_with_matches += 1;
13196
13210
  }
13197
13211
  };
13198
- const visit = (directory) => {
13199
- if (steppedOver(directory)) {
13212
+ const visit = (directory, named = false) => {
13213
+ if (!named && steppedOver(directory)) {
13200
13214
  if (gdignored(directory)) {
13201
13215
  notSearched.push(`res://${directory.slice(projectPath.length + 1).replace(/\\/g, "/")}/`);
13202
13216
  }
@@ -13219,20 +13233,17 @@ function searchProject(projectPath, options) {
13219
13233
  };
13220
13234
  const within = options.within ?? projectPath;
13221
13235
  if (statSync9(within).isDirectory()) {
13222
- visit(within);
13236
+ visit(within, true);
13223
13237
  } else {
13224
13238
  searchFile(within, basename5(within));
13225
13239
  }
13226
13240
  const answered = notSearched.length === 0 ? result : { ...result, not_searched: notSearched };
13227
- if (extensions === null || result.summary.files_searched > 0) {
13228
- return answered;
13229
- }
13230
- const asked = [...extensions].join(", ");
13231
- const there = [...met].sort().join(", ");
13232
- return {
13233
- ...answered,
13234
- note: met.size === 0 ? `No file was searched: there is no file there to search.` : `No file was searched: none there has the extension ${asked}. The extensions there are ${there}.`
13235
- };
13241
+ const around = options.within === undefined ? null : steppedOverAround(projectPath, within);
13242
+ const notes = [
13243
+ around === null ? undefined : `${around.folder} is searched because path names it; a search of the whole project steps over it, since ${around.why}.`,
13244
+ extensions === null || result.summary.files_searched > 0 ? undefined : met.size === 0 ? "No file was searched: there is no file there to search." : `No file was searched: none there has the extension ${[...extensions].join(", ")}. The extensions there are ${[...met].sort().join(", ")}.`
13245
+ ].filter((note) => note !== undefined);
13246
+ return notes.length === 0 ? answered : { ...answered, note: notes.join(" ") };
13236
13247
  }
13237
13248
  var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, SIDECAR_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
13238
13249
  var init_project_scan = __esm(() => {
@@ -16433,8 +16444,14 @@ var init_tool_definitions = __esm(() => {
16433
16444
  type: "string",
16434
16445
  description: "Variables: the initial value. Constants: the value, which a constant needs. As written in GDScript."
16435
16446
  },
16436
- isExport: { type: "boolean", description: "Variables: add @export." },
16437
- exportHint: { type: "string", description: 'Variables: the export hint, such as "range(0, 100)".' },
16447
+ isExport: {
16448
+ type: "boolean",
16449
+ description: "Variables: add @export. Default true when exportHint is given."
16450
+ },
16451
+ exportHint: {
16452
+ type: "string",
16453
+ description: 'Variables: the export hint, such as "range(0, 100)", written as @export_range(0, 100). Refused beside isExport false.'
16454
+ },
16438
16455
  isOnready: { type: "boolean", description: "Variables: add @onready." },
16439
16456
  position: {
16440
16457
  type: "string",
@@ -16537,7 +16554,7 @@ var init_tool_definitions = __esm(() => {
16537
16554
  {
16538
16555
  name: "project_settings",
16539
16556
  title: "Project settings",
16540
- description: "Reads or writes project.godot (settings, autoloads, the main scene, input actions and plugins) and the audio bus layout, default_bus_layout.tres unless audio/buses/default_bus_layout names another. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events, and its deadzone when one is given, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. unset takes a setting's line out of project.godot, the way back from a set of a misspelt name, and answers removed, old_value and default_value, the value that holds now (null for a setting the engine does not declare; for an override, name.feature, holds is the base setting's value its builds read now); on one the file does not set it answers removed false and writes nothing. Every write changes only the lines of the settings it names and keeps every other line as the file had it, in its place: the engine's own save would sort a dictionary's keys and write what a project_settings_override file laid over the project. get answers exists, true for any setting the engine knows, at_default, true when the value is the engine's own default and so has no line in project.godot, and overrides, the value each feature's override holds. An override answers override_of, its base setting, and one project.godot does not set answers exists false with holds, the base setting's value. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
16557
+ description: "Reads or writes project.godot (settings, autoloads, the main scene, input actions and plugins) and the audio bus layout, default_bus_layout.tres unless audio/buses/default_bus_layout names another. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start, and enable_plugin notes a script it would not load. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events, and its deadzone when one is given, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. unset takes a setting's line out of project.godot, the way back from a set of a misspelt name, and answers removed, old_value and default_value, the value that holds now (null for a setting the engine does not declare; for an override, name.feature, holds is the base setting's value its builds read now); on one the file does not set it answers removed false and writes nothing, its note naming the nearest settings when the engine does not declare it either. Every write changes only the lines of the settings it names and keeps every other line as the file had it, in its place: the engine's own save would sort a dictionary's keys and write what a project_settings_override file laid over the project. get answers exists, true for any setting the engine knows, at_default, true when the value is the engine's own default and so has no line in project.godot, and overrides, the value each feature's override holds. An override answers override_of, its base setting, and one project.godot does not set answers exists false with holds, the base setting's value. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
16541
16558
  parameters: {
16542
16559
  projectPath: PROJECT_PATH,
16543
16560
  setting: {
@@ -16697,7 +16714,7 @@ var init_tool_definitions = __esm(() => {
16697
16714
  name: "project_search",
16698
16715
  title: "Search the project",
16699
16716
  effect: "read",
16700
- description: "Searches text or a regular expression across project files and returns file paths with line numbers, one entry per line that holds the query, however often it holds it; total_matches counts those lines. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore, which not_searched names when the search passed one. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
16717
+ description: "Searches text or a regular expression across project files and returns file paths with line numbers, one entry per line that holds the query, however often it holds it; total_matches counts those lines. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore, which not_searched names when the search passed one. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them. A path naming one of those folders, or a place inside one, is searched as asked, and note says the whole-project search steps over it. Each line is searched on its own, so a query holding a line break is refused.",
16701
16718
  parameters: {
16702
16719
  projectPath: PROJECT_PATH,
16703
16720
  query: { type: "string", description: "The text or pattern to find." },
@@ -16712,7 +16729,11 @@ var init_tool_definitions = __esm(() => {
16712
16729
  },
16713
16730
  regex: { type: "boolean", description: "Read the query as a regular expression. Default false." },
16714
16731
  caseSensitive: { type: "boolean", description: "Default false." },
16715
- maxResults: { type: "number", description: "Default 100." }
16732
+ maxResults: {
16733
+ type: "integer",
16734
+ minimum: 1,
16735
+ description: "How many matching lines to answer. Default 100."
16736
+ }
16716
16737
  },
16717
16738
  requires: ["projectPath", "query"]
16718
16739
  },
@@ -16729,7 +16750,11 @@ var init_tool_definitions = __esm(() => {
16729
16750
  enum: ["forward", "reverse"],
16730
16751
  description: "forward: what this resource loads, with each reference closing a cycle marked circular and the cycle under circular_references, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
16731
16752
  },
16732
- depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
16753
+ depth: {
16754
+ type: "integer",
16755
+ minimum: 1,
16756
+ description: "forward: how many levels to follow, 1 being what the file loads itself. Default unlimited."
16757
+ },
16733
16758
  includeBuiltin: {
16734
16759
  type: "boolean",
16735
16760
  description: "forward: include the engine's own res://. resources. Default false."
@@ -16906,7 +16931,7 @@ var init_tool_definitions = __esm(() => {
16906
16931
  path: {
16907
16932
  type: ["string", "array"],
16908
16933
  items: { type: "string" },
16909
- 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 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. Default tests and test, those the project has, which is where gdharness test looks."
16934
+ 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. Default tests and test, those the project has, which is where gdharness test looks.'
16910
16935
  },
16911
16936
  ignore: {
16912
16937
  type: "array",
@@ -16949,7 +16974,7 @@ var init_tool_definitions = __esm(() => {
16949
16974
  rootNodeType: {
16950
16975
  type: "string",
16951
16976
  ops: ["create"],
16952
- description: "create: the root node class, an engine class or a class_name a project script declares, which the root is made as with that script attached. Default Node."
16977
+ description: "create: the root node class, an engine class or a class_name a project script declares, which the root is made as with that script attached. Default Node. The answer names the root as scene_node get does: rootNodeType its engine class, with scriptClass for a class_name."
16953
16978
  },
16954
16979
  rootNodeName: {
16955
16980
  type: "string",
@@ -16985,8 +17010,9 @@ var init_tool_definitions = __esm(() => {
16985
17010
  projectPath: PROJECT_PATH,
16986
17011
  scenePath: SCENE_PATH,
16987
17012
  depth: {
16988
- type: "number",
16989
- description: "How many levels to descend. Default: all. A node at the last level that has children of its own lists none and says how many under childrenNotListed."
17013
+ type: "integer",
17014
+ minimum: 0,
17015
+ description: "How many levels to descend, 0 being the root alone. Default: all. A node at the last level that has children of its own lists none and says how many under childrenNotListed."
16990
17016
  },
16991
17017
  includeProperties: {
16992
17018
  type: "boolean",
@@ -16994,7 +17020,7 @@ var init_tool_definitions = __esm(() => {
16994
17020
  },
16995
17021
  includeDefaults: {
16996
17022
  type: "boolean",
16997
- description: "With includeProperties, list every stored property, defaults included: a Control has about a hundred, and ten of them ran to fifty-seven thousand characters. Default false."
17023
+ description: "List every stored property, defaults included, which turns includeProperties on when it is left out: a Control has about a hundred, and ten of them ran to fifty-seven thousand characters. Default false."
16998
17024
  }
16999
17025
  },
17000
17026
  requires: ["projectPath", "scenePath"]
@@ -17019,7 +17045,7 @@ var init_tool_definitions = __esm(() => {
17019
17045
  nodeType: {
17020
17046
  type: "string",
17021
17047
  ops: ["add"],
17022
- description: `add: the node class, such as "CharacterBody2D", or a class_name a project script declares, which the node is made as with that script attached. Give this or instancePath. The answer's nodeType is the class of the node made, for an instance the class of its scene's root.`
17048
+ description: `add: the node class, such as "CharacterBody2D", or a class_name a project script declares, which the node is made as with that script attached. Give this or instancePath. The answer names the node made as get does: nodeType its engine class, for an instance the class of its scene's root, with scriptClass when its script declares a class_name.`
17023
17049
  },
17024
17050
  instancePath: {
17025
17051
  type: "string",
@@ -17117,7 +17143,7 @@ var init_tool_definitions = __esm(() => {
17117
17143
  {
17118
17144
  name: "scene_signal",
17119
17145
  title: "Scene signals",
17120
- 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 an instanced scene makes inside itself, which only that scene can change. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
17146
+ 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}`,
17121
17147
  parameters: {
17122
17148
  projectPath: PROJECT_PATH,
17123
17149
  scenePath: SCENE_PATH,
@@ -17185,7 +17211,7 @@ var init_tool_definitions = __esm(() => {
17185
17211
  animationName: {
17186
17212
  type: "string",
17187
17213
  ops: ["read", "create", "add_track", "remove_track", "set", "rename", "remove", "add_state"],
17188
- description: "The animation in the player. A name before a slash is the library it is in, as moves/walk. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. create: a library the player does not have is made in the scene and named under libraryMade. add_state: the one that state plays. One the player does not have yet is written all the same, answering animationFound false and why under animationMissing."
17214
+ description: "The animation in the player. A name before a slash is the library it is in, as moves/walk. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. create: a library the player does not have is made in the scene and named under libraryMade, where an empty name is the default library of the player, the one a name without a slash is in. add_state: the one that state plays. One the player does not have yet is written all the same, answering animationFound false and why under animationMissing."
17189
17215
  },
17190
17216
  newName: {
17191
17217
  type: "string",
@@ -17615,7 +17641,7 @@ var init_tool_definitions = __esm(() => {
17615
17641
  type: ["string", "array"],
17616
17642
  items: { type: "string" },
17617
17643
  ops: ["start"],
17618
- description: "start: gdUnit4 suites to play instead of a scene, a suite file or a test directory inside the project or a list of them, as project_test's path takes them. They run in the tests addon's suites scene through the runner gdharness test uses, so with profile: true the profile names what the suites cost by function, which project_test and gdharness test cannot show, since the profiler never sees a script run. The run quits with gdUnit4's exit code; read it with editor_output until running is false, then profile. Takes no scene or executable, and needs gdUnit4 and the tests addon of this release in the project."
17644
+ description: "start: gdUnit4 suites to play instead of a scene, a suite file or a test directory inside the project or a list of them, as project_test's path takes them, though always whole: an entry naming one case is refused. They run in the tests addon's suites scene through the runner gdharness test uses, so with profile: true the profile names what the suites cost by function, which project_test and gdharness test cannot show, since the profiler never sees a script run. The run quits with gdUnit4's exit code; read it with editor_output until running is false, then profile. Takes no scene or executable, and needs gdUnit4 and the tests addon of this release in the project."
17619
17645
  },
17620
17646
  args: {
17621
17647
  type: "array",
@@ -18419,7 +18445,7 @@ var init_tool_definitions = __esm(() => {
18419
18445
  {
18420
18446
  name: "debug_breakpoint",
18421
18447
  title: "Breakpoints",
18422
- description: "Sets, removes or lists breakpoints. A set on a script that is not there, past its last line, or on a line holding no code, blank or only a comment, is refused, as nothing would ever stop there; a remove takes any line, and with no editor connected answers removed false, saying why, when none was held there. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, for a headless run of a project whose editor plays its games with a window, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it, and the set answers once the game has taken it, which it does between frames, so a call made after the answer stops there (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
18448
+ description: "Sets, removes or lists breakpoints. A set on a script that is not there, past its last line, or on a line holding no code, blank or only a comment, is refused, as nothing would ever stop there; a remove takes any line, and with no editor connected answers removed false, saying why, when none was held there. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, for a headless run of a project whose editor plays its games with a window, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it, and the set answers once the game has taken it, which it does between frames, so a call made after the answer stops there (runningGame says so). Such a game does not stop on a script error, so a run that meets one carries on past it: editor_output has the error with where it happened, and a breakpoint on that line stops the game before the line runs. The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
18423
18449
  parameters: {
18424
18450
  projectPath: PROJECT_PATH,
18425
18451
  scriptPath: { ...SCRIPT_PATH, ops: ["set", "remove"] },
@@ -41431,7 +41457,7 @@ var init_boot_note = __esm(() => {
41431
41457
 
41432
41458
  // src/breakpoint-note.ts
41433
41459
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync26, rmSync as rmSync13, writeFileSync as writeFileSync14 } from "node:fs";
41434
- import { dirname as dirname16, isAbsolute as isAbsolute7, join as join31, relative as relative5 } from "node:path";
41460
+ import { dirname as dirname16, isAbsolute as isAbsolute7, join as join31, relative as relative6 } from "node:path";
41435
41461
  function breakpointNotePath(projectPath) {
41436
41462
  return join31(projectPath, ".godot", "gdharness-breakpoints.json");
41437
41463
  }
@@ -41446,7 +41472,7 @@ function writeBreakpointNote(projectPath, held) {
41446
41472
  }
41447
41473
  const note = {
41448
41474
  breakpoints: inside.map((one) => ({
41449
- script: relative5(root, one.scriptPath).replaceAll("\\", "/"),
41475
+ script: relative6(root, one.scriptPath).replaceAll("\\", "/"),
41450
41476
  lines: [...one.lines]
41451
41477
  }))
41452
41478
  };
@@ -41483,7 +41509,7 @@ function readBreakpointNote(projectPath) {
41483
41509
  }
41484
41510
  }
41485
41511
  function withinProject(projectPath, scriptPath) {
41486
- const rel = relative5(projectPath, scriptPath);
41512
+ const rel = relative6(projectPath, scriptPath);
41487
41513
  return rel !== "" && !rel.startsWith("..") && !isAbsolute7(rel);
41488
41514
  }
41489
41515
  var init_breakpoint_note = __esm(() => {
@@ -43376,9 +43402,9 @@ var init_rename = __esm(() => {
43376
43402
 
43377
43403
  // src/rename-symbol.ts
43378
43404
  import { existsSync as existsSync30, mkdirSync as mkdirSync17, readdirSync as readdirSync16, realpathSync as realpathSync4, rmSync as rmSync16, writeFileSync as writeFileSync18 } from "node:fs";
43379
- import { basename as basename9, dirname as dirname21, join as join36, relative as relative6, resolve as resolve12 } from "node:path";
43405
+ import { basename as basename9, dirname as dirname21, join as join36, relative as relative7, resolve as resolve12 } from "node:path";
43380
43406
  function resourcePathOf(projectPath, absolute) {
43381
- const inside = relative6(onDiskSpelling(projectPath), onDiskSpelling(absolute));
43407
+ const inside = relative7(onDiskSpelling(projectPath), onDiskSpelling(absolute));
43382
43408
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
43383
43409
  return null;
43384
43410
  }
@@ -43884,7 +43910,7 @@ import {
43884
43910
  statSync as statSync17,
43885
43911
  writeFileSync as writeFileSync19
43886
43912
  } from "node:fs";
43887
- import { basename as basename10, dirname as dirname22, extname as extname3, join as join37, relative as relative7 } from "node:path";
43913
+ import { basename as basename10, dirname as dirname22, extname as extname3, join as join37, relative as relative8 } from "node:path";
43888
43914
  function filesUnder(directory) {
43889
43915
  return readdirSync17(directory, { withFileTypes: true }).flatMap((entry) => {
43890
43916
  const path = join37(directory, entry.name);
@@ -43945,7 +43971,7 @@ function binaryResources(projectPath) {
43945
43971
  return found;
43946
43972
  }
43947
43973
  function targetPathOf(projectPath, absolute) {
43948
- const inside = relative7(onDiskSpelling(projectPath), join37(onDiskSpelling(dirname22(absolute)), basename10(absolute)));
43974
+ const inside = relative8(onDiskSpelling(projectPath), join37(onDiskSpelling(dirname22(absolute)), basename10(absolute)));
43949
43975
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
43950
43976
  return null;
43951
43977
  }
@@ -47908,7 +47934,14 @@ function similarity(one, other) {
47908
47934
  const shared = ones.filter((pair) => others.includes(pair)).length;
47909
47935
  return 2 * shared / (ones.length + others.length);
47910
47936
  }
47911
- function withNearestFiles(said, wanted, files) {
47937
+ function withNearestFiles(said, wanted, files, beside) {
47938
+ const stem = (path) => (path.split("/").pop() ?? path).replace(/\.[^.]*$/, "");
47939
+ const named = said.split(/[\s,;]+/).map((word) => word.replace(/[.:]+$/, ""));
47940
+ const namesakes = beside.filter((file) => file !== wanted && stem(file) === stem(wanted) && !named.includes(file)).sort();
47941
+ if (namesakes.length > 0) {
47942
+ const alone = namesakes.length === 1;
47943
+ return `${said}. The ${alone ? "file" : "files"} of that name beside it ${alone ? "is" : "are"} ${namesakes.join(", ")}.`;
47944
+ }
47912
47945
  const extension = /\.([^./]+)$/.exec(wanted)?.[1];
47913
47946
  if (extension === undefined) {
47914
47947
  return said;
@@ -47916,7 +47949,6 @@ function withNearestFiles(said, wanted, files) {
47916
47949
  if (files.length === 0) {
47917
47950
  return `${said}. The project holds no .${extension} file.`;
47918
47951
  }
47919
- const stem = (path) => (path.split("/").pop() ?? path).replace(/\.[^.]*$/, "");
47920
47952
  const close = nearest(stem(wanted), files.map((file) => stem(file)));
47921
47953
  const offered = files.filter((file) => close.includes(stem(file)));
47922
47954
  if (offered.length > 0) {
@@ -48894,7 +48926,7 @@ var init_server_instructions = __esm(() => {
48894
48926
 
48895
48927
  // src/suites-scene.ts
48896
48928
  import { existsSync as existsSync32, mkdirSync as mkdirSync22, writeFileSync as writeFileSync21 } from "node:fs";
48897
- import { join as join41, relative as relative8, sep as sep4 } from "node:path";
48929
+ import { join as join41, relative as relative9, sep as sep5 } from "node:path";
48898
48930
  function hasSuitesScene(projectPath) {
48899
48931
  return existsSync32(join41(projectPath, SUITES_SCENE));
48900
48932
  }
@@ -48918,7 +48950,7 @@ function suitesSceneArgs(projectPath, suites) {
48918
48950
  "-c",
48919
48951
  "--ignoreHeadlessMode",
48920
48952
  "-rd",
48921
- `res://${relative8(projectPath, join41(directory, "reports")).split(sep4).join("/")}`,
48953
+ `res://${relative9(projectPath, join41(directory, "reports")).split(sep5).join("/")}`,
48922
48954
  "-rc",
48923
48955
  "1"
48924
48956
  ];
@@ -48933,6 +48965,44 @@ var init_suites_scene = __esm(() => {
48933
48965
  // src/test-selection.ts
48934
48966
  import { existsSync as existsSync33 } from "node:fs";
48935
48967
  import { basename as basename12, join as join42 } from "node:path";
48968
+ function casesOutOfPath(projectPath, given, tests) {
48969
+ const entries = Array.isArray(given) ? given : [given];
48970
+ const cases = [];
48971
+ const whole = [];
48972
+ const path = entries.map((entry) => {
48973
+ const caseAt = typeof entry === "string" ? CASE_AFTER_SCRIPT.exec(entry.trim()) : null;
48974
+ const suite = caseAt === null ? null : resolveWithinProject(projectPath, caseAt[1] ?? "");
48975
+ if (caseAt === null || suite === null || !suite.ok) {
48976
+ if (typeof entry === "string") {
48977
+ whole.push(entry);
48978
+ }
48979
+ return entry;
48980
+ }
48981
+ cases.push(`res://${suite.relativePath}:${caseAt[2] ?? ""}`);
48982
+ return `res://${suite.relativePath}`;
48983
+ });
48984
+ if (cases.length === 0) {
48985
+ return { ok: true, path: given, tests };
48986
+ }
48987
+ if (whole.length > 0) {
48988
+ return {
48989
+ ok: false,
48990
+ reason: `path names ${cases.length === 1 ? "a case" : "cases"}, ${cases.join(", ")}, beside ${whole.join(", ")}, and a run with cases named runs only those: run the cases on their own, and the rest in a run of its own.`
48991
+ };
48992
+ }
48993
+ const named = tests === undefined || tests === null ? [] : Array.isArray(tests) ? tests : [tests];
48994
+ return { ok: true, path: [...new Set(path)], tests: [...named, ...cases] };
48995
+ }
48996
+ function caseInSuites(given) {
48997
+ const entries = Array.isArray(given) ? given : [given];
48998
+ const cased = entries.filter((entry) => typeof entry === "string" && CASE_AFTER_SCRIPT.test(entry.trim()));
48999
+ const first = cased[0];
49000
+ if (first === undefined) {
49001
+ return null;
49002
+ }
49003
+ const file = CASE_AFTER_SCRIPT.exec(first.trim())?.[1] ?? first;
49004
+ return `suites plays whole suites, and ${cased.join(", ")} ${cased.length === 1 ? "names" : "name"} one case: name the file, ${file}, to play every case in it, or run the case alone with project_test, whose path takes ${first}.`;
49005
+ }
48936
49006
  function casesIn(scripts, suite) {
48937
49007
  return scripts.suite(suite)?.cases ?? scripts.script(suite)?.funcs.filter((name) => name.startsWith("test_")) ?? [];
48938
49008
  }
@@ -48994,9 +49064,12 @@ function selectTests(projectPath, asked, given) {
48994
49064
  const ignore = declared.filter((one) => suites.includes(one.suite) && !chosen.has(`${one.suite}:${one.test}`)).map((one) => `${one.suite}:${one.test}`);
48995
49065
  return { ok: true, value: { suites, ignore, selected } };
48996
49066
  }
49067
+ var CASE_AFTER_SCRIPT;
48997
49068
  var init_test_selection = __esm(() => {
49069
+ init_paths();
48998
49070
  init_project_scan();
48999
49071
  init_suite_plan();
49072
+ CASE_AFTER_SCRIPT = /^(.*\.gd):(\w+)$/;
49000
49073
  });
49001
49074
 
49002
49075
  // src/server.ts
@@ -49018,7 +49091,7 @@ import {
49018
49091
  statSync as statSync20,
49019
49092
  writeFileSync as writeFileSync22
49020
49093
  } from "node:fs";
49021
- import { basename as basename13, dirname as dirname25, extname as extname4, isAbsolute as isAbsolute8, join as join43, normalize as normalize2, relative as relative9 } from "node:path";
49094
+ import { basename as basename13, dirname as dirname25, extname as extname4, isAbsolute as isAbsolute8, join as join43, normalize as normalize2, relative as relative10 } from "node:path";
49022
49095
  import { setTimeout as delay7 } from "node:timers/promises";
49023
49096
  import { fileURLToPath as fileURLToPath5 } from "node:url";
49024
49097
  function editorRestartTimeoutMs() {
@@ -49175,7 +49248,7 @@ function keptGameRequest(tool, op, args) {
49175
49248
  }
49176
49249
  }
49177
49250
  function projectSpelling(projectPath, absolutePath) {
49178
- return `res://${relative9(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
49251
+ return `res://${relative10(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
49179
49252
  }
49180
49253
  function whichGame(args) {
49181
49254
  return { projectPath: args["projectPath"], pid: args["pid"] };
@@ -50082,7 +50155,7 @@ function failedVerdict(report) {
50082
50155
  const last = parts.pop();
50083
50156
  return last === undefined ? "failures" : parts.length === 0 ? last : `${parts.join(", ")} and ${last}`;
50084
50157
  }
50085
- function timedOutVerdict(timeoutMs, silentForMs, repeating = null) {
50158
+ function timedOutVerdict(timeoutMs, silentForMs, repeating = null, printedAnything = true) {
50086
50159
  const quiet = Math.round(silentForMs);
50087
50160
  if (repeating !== null) {
50088
50161
  return {
@@ -50090,6 +50163,12 @@ function timedOutVerdict(timeoutMs, silentForMs, repeating = null) {
50090
50163
  hung: false
50091
50164
  };
50092
50165
  }
50166
+ if (!printedAnything && timeoutMs < STALLED_AFTER_MS) {
50167
+ return {
50168
+ verdict: `timed out after ${timeoutMs} ms before the engine printed anything, so it may still have been starting: timeoutMs counts milliseconds, and the default is ${TEST_TIMEOUT_MS}`,
50169
+ hung: false
50170
+ };
50171
+ }
50093
50172
  return quiet >= Math.min(STALLED_AFTER_MS, timeoutMs / 2) ? {
50094
50173
  verdict: `hung: killed after ${timeoutMs} ms, having printed nothing for the last ${quiet} ms`,
50095
50174
  hung: true
@@ -51318,13 +51397,20 @@ class GodotServer {
51318
51397
  return await this.handleRunTests(args);
51319
51398
  case "scene_create":
51320
51399
  return op === "create" ? await editorOrEngine("create_scene", "writes") : await editorOrEngine("save_scene", "writes");
51321
- case "scene_tree":
51400
+ case "scene_tree": {
51401
+ const defaults = readBoolean(args, "includeDefaults") ?? false;
51402
+ const asked = readBoolean(args, "includeProperties");
51403
+ if (defaults && asked === false) {
51404
+ return this.createErrorResponse("includeDefaults lists the defaults among the properties, and includeProperties false lists none: leave includeProperties out, or set it true.");
51405
+ }
51406
+ const properties = asked ?? defaults;
51322
51407
  return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
51323
51408
  scenePath: file,
51324
51409
  depth: readNumber(args, "depth") ?? -1,
51325
- includeProperties: readBoolean(args, "includeProperties") ?? false,
51326
- includeDefaults: readBoolean(args, "includeDefaults") ?? false
51327
- }), async () => await bridge("list_scene_nodes"));
51410
+ includeProperties: properties,
51411
+ includeDefaults: defaults
51412
+ }), async () => await bridge("list_scene_nodes", { includeProperties: properties }));
51413
+ }
51328
51414
  case "scene_node":
51329
51415
  switch (op) {
51330
51416
  case "add":
@@ -52481,6 +52567,9 @@ class GodotServer {
52481
52567
  if (!project.ok) {
52482
52568
  return project.response;
52483
52569
  }
52570
+ if (/[\r\n]/.test(readString(args, "query") ?? "")) {
52571
+ return this.createErrorResponse("query holds a line break, and project_search matches one line at a time: search for one of its lines.");
52572
+ }
52484
52573
  const given = readNonEmptyString(args, "path");
52485
52574
  const place = given === undefined ? null : resolveWithinProject(project.value.path, given, { root: true });
52486
52575
  if (place !== null && !place.ok) {
@@ -52730,10 +52819,18 @@ ${platform}`;
52730
52819
  ticker.unref();
52731
52820
  return ticker;
52732
52821
  }
52733
- async handleRunTests(args) {
52822
+ async handleRunTests(given) {
52823
+ const project = this.project(given);
52824
+ if (!project.ok) {
52825
+ return project.response;
52826
+ }
52827
+ const split = casesOutOfPath(project.value.path, given["path"], given["tests"]);
52828
+ if (!split.ok) {
52829
+ return this.createErrorResponse(split.reason);
52830
+ }
52831
+ const args = split.path === given["path"] ? given : { ...given, path: split.path, tests: split.tests };
52734
52832
  const ran = await this.runTests(args);
52735
- const project = this.project(args);
52736
- if (!project.ok || callSignal()?.aborted === true) {
52833
+ if (callSignal()?.aborted === true) {
52737
52834
  return ran;
52738
52835
  }
52739
52836
  const paths = testPathsIn(project.value.path, args["path"]);
@@ -52892,7 +52989,7 @@ ${platform}`;
52892
52989
  "--expected-out",
52893
52990
  expectedFile
52894
52991
  ];
52895
- const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
52992
+ const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? TEST_TIMEOUT_MS;
52896
52993
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
52897
52994
  const helperFiles = {
52898
52995
  spec: join43(userData, "spec"),
@@ -52905,12 +53002,15 @@ ${platform}`;
52905
53002
  const run = await this.spawnGame(engine.value, cmdArgs, userDataIn(userData), onDesktop ? { directory: project.value.path, files: helperFiles } : undefined);
52906
53003
  const console2 = [];
52907
53004
  let lastSaid = Date.now();
53005
+ let saidAnything = false;
52908
53006
  run.process.stdout?.on("data", (data) => {
52909
53007
  console2.push(data);
52910
53008
  lastSaid = Date.now();
53009
+ saidAnything = true;
52911
53010
  });
52912
53011
  run.process.stderr?.on("data", () => {
52913
53012
  lastSaid = Date.now();
53013
+ saidAnything = true;
52914
53014
  });
52915
53015
  const reporting = this.reportTestProgress(run.log);
52916
53016
  const autoloaded = scriptsAutoloaded(project.value.path);
@@ -53003,7 +53103,7 @@ ${platform}`;
53003
53103
  }).entries;
53004
53104
  const said = printed.map((entry) => entry.text);
53005
53105
  const nothingRan = timedOut || notCompiled !== null ? null : whyNoReport(said, asked.join(" and "));
53006
- const cutShort = timedOut && notCompiled === null ? timedOutVerdict(timeoutMs, silentForMs, repeatingScriptError(run.log.all, (entry) => run.log.raisedByScript(entry))) : null;
53106
+ const cutShort = timedOut && notCompiled === null ? timedOutVerdict(timeoutMs, silentForMs, repeatingScriptError(run.log.all, (entry) => run.log.raisedByScript(entry)), saidAnything) : null;
53007
53107
  const hung = cutShort?.hung ?? false;
53008
53108
  const printedErrors = notCompiled !== null ? compileErrorsIn(run.log.all) : timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
53009
53109
  const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
@@ -53356,7 +53456,10 @@ ${error.text}`;
53356
53456
  missingScript(projectPath, relativePath) {
53357
53457
  const scripts = projectFiles(projectPath, [".gd"]).map((script) => `res://${script}`);
53358
53458
  const wanted = `res://${relativePath}`;
53359
- return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts));
53459
+ const folder = dirname25(relativePath.replaceAll("\\", "/"));
53460
+ const inFolder = join43(projectPath, folder);
53461
+ const beside = existsSync34(inFolder) ? readdirSync19(inFolder, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `res://${folder === "." ? "" : `${folder}/`}${entry.name}`) : [];
53462
+ return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts, beside));
53360
53463
  }
53361
53464
  async compileScripts(projectPath, paths) {
53362
53465
  const payload = { checked: [], failed: [], scripts: [] };
@@ -54437,6 +54540,10 @@ ${error.text}`;
54437
54540
  if (args["scene"] !== undefined || args["executable"] !== undefined) {
54438
54541
  return refused("suites plays the suites in the tests addon's own scene, so it takes no scene and no executable: leave those out.");
54439
54542
  }
54543
+ const cased = caseInSuites(args["suites"]);
54544
+ if (cased !== null) {
54545
+ return refused(cased);
54546
+ }
54440
54547
  const paths = testPathsIn(projectPath, args["suites"], "suites");
54441
54548
  if (!paths.ok) {
54442
54549
  return refused(paths.reason, PATH_SOLUTIONS);
@@ -54626,6 +54733,9 @@ ${error.text}`;
54626
54733
  let editorAskedAt = 0;
54627
54734
  let editorSaysGoing = true;
54628
54735
  let editorAsks = 0;
54736
+ let holding = null;
54737
+ let keeperAskedAt = 0;
54738
+ let heldHere = null;
54629
54739
  const waitBegan = Date.now();
54630
54740
  const runBegan = Math.min(this.activeProcess?.startedAt ?? waitBegan, waitBegan);
54631
54741
  const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
@@ -54638,7 +54748,19 @@ ${error.text}`;
54638
54748
  }
54639
54749
  const run = this.activeProcess;
54640
54750
  if (run === null || !run.throughEditor) {
54641
- return !stillRunning(run);
54751
+ if (!stillRunning(run)) {
54752
+ return true;
54753
+ }
54754
+ const keeper = run?.keeper;
54755
+ if (keeper !== undefined && holding === null && Date.now() - keeperAskedAt >= RUN_POLL_MS) {
54756
+ keeperAskedAt = Date.now();
54757
+ holding = this.keptHold(keeper).then((held) => {
54758
+ heldHere = held;
54759
+ }).finally(() => {
54760
+ holding = null;
54761
+ });
54762
+ }
54763
+ return heldHere !== null;
54642
54764
  }
54643
54765
  if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
54644
54766
  editorAskedAt = Date.now();
@@ -54674,7 +54796,7 @@ ${error.text}`;
54674
54796
  addon: true,
54675
54797
  budgetMs,
54676
54798
  ...budget.sizedToMs === undefined ? {} : { sizedToMs: budget.sizedToMs },
54677
- heldAt: this.dapClient?.whereItStopped() ?? null,
54799
+ heldAt: going === null || going.throughEditor ? this.dapClient?.whereItStopped() ?? null : endpoint === null && running && going.keeper !== undefined ? await this.keptHold(going.keeper) : null,
54678
54800
  running,
54679
54801
  withArgs,
54680
54802
  servesAProject: this.ownProject !== null,
@@ -56762,7 +56884,12 @@ ${error.text}`;
56762
56884
  return contained.response;
56763
56885
  }
56764
56886
  const { op: _op, projectPath: _projectPath, ...params } = contained.value;
56765
- const created = await this.operation("create_script", params, project.value.path);
56887
+ const stale = await this.refreshStaleClasses(project.value.path);
56888
+ if (!stale.ok) {
56889
+ return stale.response;
56890
+ }
56891
+ const loaded = await this.operation("create_script", params, project.value.path);
56892
+ const created = loaded.ok && stale.value.length > 0 ? { ...loaded, payload: { ...loaded.payload, classesRebuiltFirst: stale.value } } : loaded;
56766
56893
  const declared = created.ok ? readString(created.payload, "class_name") ?? "" : "";
56767
56894
  if (!created.ok || declared === "") {
56768
56895
  return this.answer(created);
@@ -56799,8 +56926,26 @@ ${error.text}`;
56799
56926
  });
56800
56927
  }
56801
56928
  async handleModifyScript(args) {
56802
- const written = await this.headless("modify_script", args);
56803
56929
  const project = this.project(args);
56930
+ const stale = project.ok ? await this.refreshStaleClasses(project.value.path) : null;
56931
+ if (stale !== null && !stale.ok) {
56932
+ return stale.response;
56933
+ }
56934
+ const loaded = await this.headless("modify_script", args);
56935
+ const first = loaded.content[0];
56936
+ const written = stale === null || stale.value.length === 0 || loaded.isError === true || first?.type !== "text" || !first.text.trimStart().startsWith("{") ? loaded : {
56937
+ ...loaded,
56938
+ content: [
56939
+ {
56940
+ ...first,
56941
+ text: answerJson({
56942
+ ...asParams(JSON.parse(first.text)),
56943
+ classesRebuiltFirst: stale.value
56944
+ })
56945
+ },
56946
+ ...loaded.content.slice(1)
56947
+ ]
56948
+ };
56804
56949
  const scriptPath = readNonEmptyString(args, "scriptPath");
56805
56950
  if (written.isError === true || !project.ok || scriptPath === undefined || !this.editorHasOpen(project.value.path)) {
56806
56951
  return written;
@@ -57777,7 +57922,7 @@ async function runGodotServer() {
57777
57922
  const server = new GodotServer;
57778
57923
  await server.run();
57779
57924
  }
57780
- 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, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, 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;
57925
+ 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, 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;
57781
57926
  var init_server = __esm(() => {
57782
57927
  init_dist2();
57783
57928
  init_stdio();
@@ -58781,7 +58926,7 @@ import {
58781
58926
  writeFileSync as writeFileSync12
58782
58927
  } from "node:fs";
58783
58928
  import { availableParallelism as availableParallelism4 } from "node:os";
58784
- import { dirname as dirname13, join as join27, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
58929
+ import { dirname as dirname13, join as join27, relative as relative5, resolve as resolve10, sep as sep4 } from "node:path";
58785
58930
  import process14 from "node:process";
58786
58931
 
58787
58932
  // src/suite-fingerprint.ts
@@ -58946,6 +59091,7 @@ function getLocalVersion() {
58946
59091
  }
58947
59092
 
58948
59093
  // src/suite-run.ts
59094
+ var LAST_RUN_FILE = "last-run.json";
58949
59095
  var SLOWEST_NAMED = 5;
58950
59096
  var SILENCE_TIMES_TAKEN = 4;
58951
59097
  function silenceWhile(running, times, floorSeconds) {
@@ -59116,14 +59262,14 @@ class RunLogs {
59116
59262
  return this.kept ?? this.directory;
59117
59263
  }
59118
59264
  named(path) {
59119
- return this.kept === null ? path : join27(this.kept, relative4(this.directory, path));
59265
+ return this.kept === null ? path : join27(this.kept, relative5(this.directory, path));
59120
59266
  }
59121
59267
  keep() {
59122
59268
  if (this.kept === null) {
59123
59269
  return;
59124
59270
  }
59125
59271
  for (const file of readdirSync14(this.directory, { recursive: true, encoding: "utf8" })) {
59126
- if (file.endsWith(".log") && file.split(sep3).length === 2) {
59272
+ if (file.endsWith(".log") && file.split(sep4).length === 2) {
59127
59273
  mkdirSync10(dirname13(join27(this.kept, file)), { recursive: true });
59128
59274
  copyFileSync(join27(this.directory, file), join27(this.kept, file));
59129
59275
  }
@@ -59141,6 +59287,7 @@ async function runTestCommand(options, godotPath) {
59141
59287
  const state = join27(home, ".godot", "gdharness");
59142
59288
  const timesFile = join27(state, "suite-times.json");
59143
59289
  const passedFile = join27(state, "suite-passed.json");
59290
+ const lastRunFile = join27(state, LAST_RUN_FILE);
59144
59291
  let suites = selectedSuites(scripts, options);
59145
59292
  const uncollected = uncollectedSaid(scripts.uncollectedUnder(searchedRoots(options)));
59146
59293
  const wholeSuites = suites.map((suite) => scripts.suite(suite.path) ?? suite);
@@ -59188,7 +59335,7 @@ async function runTestCommand(options, godotPath) {
59188
59335
  const imported = await importProject(launch);
59189
59336
  if (imported !== null) {
59190
59337
  logs.keep();
59191
- writeEarlyResult(options.resultFile, startedAt, [`the import failed: ${imported}`], logs.shown);
59338
+ writeEarlyResult({ resultFile: options.resultFile, lastRunFile, engines: options.engines }, startedAt, [`the import failed: ${imported}`], logs.shown);
59192
59339
  console.log(`The import failed, so no suite ran: ${imported}`);
59193
59340
  return 1;
59194
59341
  }
@@ -59213,7 +59360,7 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59213
59360
  if (suites.length === 0) {
59214
59361
  console.log("Nothing has changed since every suite last passed.");
59215
59362
  rmSync12(runDirectory, { recursive: true, force: true });
59216
- writeEarlyResult(options.resultFile, startedAt, uncollected, null);
59363
+ writeEarlyResult({ resultFile: options.resultFile, lastRunFile, engines: options.engines }, startedAt, uncollected, null);
59217
59364
  if (uncollected.length === 0) {
59218
59365
  return 0;
59219
59366
  }
@@ -59239,26 +59386,31 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59239
59386
  const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
59240
59387
  const asked = unitsOf(suites, times, splitPastFor(suites, times, options.engines));
59241
59388
  const how = options.order === null ? `${windowedSaid(plans)}${roomSaid(plans.length, lanesOf(plans), lanesOf(dealt(asked, declared, options.engines)), room.held, options.engines)}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
59242
- console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
59389
+ const dealing = [];
59390
+ const say = (line) => {
59391
+ dealing.push(line);
59392
+ console.log(line);
59393
+ };
59394
+ say(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
59243
59395
  const windowedAnyway = headlessInAWindow(plans, units);
59244
59396
  if (windowedAnyway.length > 0) {
59245
59397
  const one = windowedAnyway.length === 1;
59246
- console.log(`${named(windowedAnyway)} ${one ? "says it runs" : "say they run"} headless and ${one ? "runs" : "run"} with a window here, since an ordered run is one engine and a suite in it needs a window. Run ${one ? "it" : "them"} without the windowed suites to run ${one ? "it" : "them"} headless.`);
59398
+ say(`${named(windowedAnyway)} ${one ? "says it runs" : "say they run"} headless and ${one ? "runs" : "run"} with a window here, since an ordered run is one engine and a suite in it needs a window. Run ${one ? "it" : "them"} without the windowed suites to run ${one ? "it" : "them"} headless.`);
59247
59399
  }
59248
59400
  const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path && unit.row === undefined).length > 1);
59249
59401
  if (plans.length > 1 && split.length > 0) {
59250
- console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(split)}.`);
59402
+ say(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(split)}.`);
59251
59403
  }
59252
59404
  const byRows = [
59253
59405
  ...new Set(units.flatMap((unit) => unit.row === undefined ? [] : [`${unit.suite} > ${unit.cases[0] ?? ""}`]))
59254
59406
  ];
59255
59407
  if (plans.length > 1 && byRows.length > 0) {
59256
- console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(byRows)}.`);
59408
+ say(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(byRows)}.`);
59257
59409
  }
59258
59410
  const keptWhole = units.flatMap((unit) => unit.cost > splitPast && !unit.windowed && suites.some((s) => s.path === unit.suite && s.together) ? [unit.suite] : []);
59259
59411
  if (options.order === null && room.engines > 1 && keptWhole.length > 0) {
59260
59412
  const one = keptWhole.length === 1;
59261
- console.log(`Kept whole in one engine, since ${one ? "it declares" : "each declares"} GDHARNESS_TOGETHER: ${named(keptWhole)}.`);
59413
+ say(`Kept whole in one engine, since ${one ? "it declares" : "each declares"} GDHARNESS_TOGETHER: ${named(keptWhole)}.`);
59262
59414
  }
59263
59415
  const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
59264
59416
  const taken = [];
@@ -59271,11 +59423,11 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59271
59423
  }
59272
59424
  const shared = options.order === null && lanes.length > 1 && taken.length < lanes.length;
59273
59425
  if (shared) {
59274
- console.log(sharesSaid(taken.length, lanes.length));
59426
+ say(sharesSaid(taken.length, lanes.length));
59275
59427
  }
59276
59428
  let ran = shared ? await runShares(launch, units, declared, lanes.length, taken, times) : (await Promise.all(lanes.map((lane, at) => runLane(launch, lane, times, taken[at] ?? null)))).flat().sort((a, b) => a.index - b.index);
59277
59429
  if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
59278
- console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
59430
+ say("An engine ran out of memory, so every engine that failed runs again, one at a time.");
59279
59431
  const again = [];
59280
59432
  for (const engine of ran) {
59281
59433
  again.push(engine.verdict.passed ? engine : await runEngine2(launch, engine.plan, engine.index, times, "again"));
@@ -59285,7 +59437,13 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59285
59437
  writeJson(timesFile, timesAfter(readTimes(timesFile), wholeSuites, ran.map((engine) => engine.printed), (path) => scripts.suite(path), new Set(ran.flatMap((engine) => engine.plan.suites.filter((suite) => (engine.printed.suites.get(suite.path)?.closed ?? null) === null).flatMap((suite) => suite.cases.map((func) => `${suite.path}:${func}`))))));
59286
59438
  const across = rowsAcross(ran.map((engine) => ({ handed: engine.plan.suites, printed: engine.printed })));
59287
59439
  recordPassed(passedFile, ran, printsOf, new Set(across.map((fault) => fault.suite)));
59288
- return report(ran, across.map((fault) => fault.fault), uncollected, startedAt, logs, options.printOutput, options.resultFile);
59440
+ return report(ran, across.map((fault) => fault.fault), uncollected, startedAt, logs, options.printOutput, {
59441
+ resultFile: options.resultFile,
59442
+ lastRunFile,
59443
+ dealing,
59444
+ engines: options.engines,
59445
+ slotsFree: taken.length
59446
+ });
59289
59447
  } finally {
59290
59448
  process14.off("SIGINT", interrupted);
59291
59449
  process14.off("SIGTERM", interrupted);
@@ -59450,7 +59608,7 @@ async function runEngine2(launch, plan, index, times, again = "", slot = null) {
59450
59608
  ...plan.windowed ? [] : ["--ignoreHeadlessMode"],
59451
59609
  "-c",
59452
59610
  "-rd",
59453
- `res://${relative4(launch.projectPath, join27(directory, "reports")).split(sep3).join("/")}`,
59611
+ `res://${relative5(launch.projectPath, join27(directory, "reports")).split(sep4).join("/")}`,
59454
59612
  "-rc",
59455
59613
  "1",
59456
59614
  "--",
@@ -59492,6 +59650,7 @@ async function runEngine2(launch, plan, index, times, again = "", slot = null) {
59492
59650
  printed,
59493
59651
  verdict: written === null ? judged : { ...judged, passed: false, faults: [...judged.faults, written] },
59494
59652
  log,
59653
+ began,
59495
59654
  seconds: (Date.now() - began) / 1000,
59496
59655
  again: again !== ""
59497
59656
  };
@@ -59502,11 +59661,11 @@ function reportsFault(reports) {
59502
59661
  if (files.length === 0) {
59503
59662
  return null;
59504
59663
  }
59505
- const shown = files.slice(0, REPORT_FILES_NAMED).map((file) => file.split(sep3).join("/"));
59664
+ const shown = files.slice(0, REPORT_FILES_NAMED).map((file) => file.split(sep4).join("/"));
59506
59665
  const more = files.length - shown.length;
59507
59666
  return `gdUnit4 wrote ${plural(files.length, "file")} into ${reports} (${shown.join(", ")}${more > 0 ? ` and ${String(more)} more` : ""}), a report the runner had turned off, so the gdUnit4 installed no longer honours a session hook marked off; report it to gdharness with gdUnit4's version`;
59508
59667
  }
59509
- function report(ran, across, uncollected, startedAt, logs, printOutput, resultFile) {
59668
+ function report(ran, across, uncollected, startedAt, logs, printOutput, targets) {
59510
59669
  if (printOutput) {
59511
59670
  for (const engine of ran) {
59512
59671
  console.log("");
@@ -59583,27 +59742,31 @@ ${readFileSync22(engine.log, "utf8")}`);
59583
59742
  } else {
59584
59743
  logs.keep();
59585
59744
  }
59586
- if (resultFile !== null) {
59587
- writeJson(resultFile, {
59588
- passed,
59589
- seconds: took,
59590
- problems: [
59591
- ...failed.flatMap((engine) => engine.verdict.faults.map((fault) => `engine ${String(engine.index)}: ${fault}`)),
59592
- ...across,
59593
- ...uncollected
59594
- ],
59595
- suites: closed,
59596
- engines: ran.map((engine) => ({
59597
- index: engine.index,
59598
- seconds: engine.seconds,
59599
- dealt: engine.plan.cost,
59600
- passed: engine.verdict.passed,
59601
- windowed: engine.plan.windowed,
59602
- log: passed ? null : logs.named(engine.log)
59603
- })),
59604
- logs: passed ? null : logs.shown
59605
- });
59606
- }
59745
+ writeResult(targets, {
59746
+ passed,
59747
+ seconds: took,
59748
+ problems: [
59749
+ ...failed.flatMap((engine) => engine.verdict.faults.map((fault) => `engine ${String(engine.index)}: ${fault}`)),
59750
+ ...across,
59751
+ ...uncollected
59752
+ ],
59753
+ suites: closed,
59754
+ engines: ran.map((engine) => ({
59755
+ index: engine.index,
59756
+ lane: engine.plan.lane,
59757
+ startedAfter: (engine.began - startedAt) / 1000,
59758
+ seconds: engine.seconds,
59759
+ dealt: engine.plan.cost,
59760
+ passed: engine.verdict.passed,
59761
+ windowed: engine.plan.windowed,
59762
+ suites: engine.plan.suites.map((suite) => suite.path),
59763
+ log: passed ? null : logs.named(engine.log)
59764
+ })),
59765
+ logs: passed ? null : logs.shown,
59766
+ enginesAsked: targets.engines,
59767
+ slotsFree: targets.slotsFree,
59768
+ dealing: targets.dealing
59769
+ });
59607
59770
  if (passed) {
59608
59771
  console.log(`Passed: ${totals}`);
59609
59772
  return 0;
@@ -59613,16 +59776,23 @@ ${readFileSync22(engine.log, "utf8")}`);
59613
59776
  console.log(`Every engine's output is kept under ${logs.shown} until ${String(KEPT_RUN_DIRECTORIES)} later runs have failed.`);
59614
59777
  return 1;
59615
59778
  }
59616
- function writeEarlyResult(file, startedAt, problems, logs) {
59617
- if (file !== null) {
59618
- writeJson(file, {
59619
- passed: problems.length === 0,
59620
- seconds: (Date.now() - startedAt) / 1000,
59621
- problems,
59622
- suites: [],
59623
- engines: [],
59624
- logs
59625
- });
59779
+ function writeEarlyResult(targets, startedAt, problems, logs) {
59780
+ writeResult(targets, {
59781
+ passed: problems.length === 0,
59782
+ seconds: (Date.now() - startedAt) / 1000,
59783
+ problems,
59784
+ suites: [],
59785
+ engines: [],
59786
+ logs,
59787
+ enginesAsked: targets.engines,
59788
+ slotsFree: 0,
59789
+ dealing: []
59790
+ });
59791
+ }
59792
+ function writeResult(targets, result) {
59793
+ writeJson(targets.lastRunFile, result);
59794
+ if (targets.resultFile !== null) {
59795
+ writeJson(targets.resultFile, result);
59626
59796
  }
59627
59797
  }
59628
59798
  function printUncollected(uncollected) {