gdharness 1.1.44 → 1.1.46

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/index.js CHANGED
@@ -33085,13 +33085,13 @@ var TOOL_SPECS = [
33085
33085
  },
33086
33086
  {
33087
33087
  name: "project_test",
33088
- description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
33088
+ description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
33089
33089
  parameters: {
33090
33090
  projectPath: PROJECT_PATH,
33091
33091
  path: {
33092
33092
  type: ["string", "array"],
33093
33093
  items: { type: "string" },
33094
- 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. A run where any entry is not there is refused before the engine starts, naming each one. Default test."
33094
+ 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. Default test."
33095
33095
  },
33096
33096
  ignore: {
33097
33097
  type: "array",
@@ -33405,7 +33405,7 @@ var TOOL_SPECS = [
33405
33405
  },
33406
33406
  {
33407
33407
  name: "editor_run",
33408
- description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
33408
+ description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for, and the note then says what a runtime_* call naming no projectPath meets until it announces. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
33409
33409
  parameters: {
33410
33410
  projectPath: {
33411
33411
  ...PROJECT_PATH,
@@ -33621,7 +33621,7 @@ var TOOL_SPECS = [
33621
33621
  property: {
33622
33622
  type: "string",
33623
33623
  ops: ["property", "find"],
33624
- 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 packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and 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. 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.'
33624
+ 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 packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. 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.'
33625
33625
  },
33626
33626
  depth: {
33627
33627
  type: "integer",
@@ -33718,7 +33718,7 @@ var TOOL_SPECS = [
33718
33718
  property: {
33719
33719
  type: "string",
33720
33720
  ops: ["set"],
33721
- description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. A map keyed by numbers is stepped into by the number, "_game:units:3". The answer reads back off the same holder, and a write that leaves the property as it was is refused and says what it still holds: the engine not taking it and a setter keeping the value look the same, so the refusal names neither. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
33721
+ description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a reading method returned, as runtime_inspect property describes. A map keyed by numbers is stepped into by the number, "_game:units:3". The answer reads back off the same holder, and a write that leaves the property as it was is refused and says what it still holds: the engine not taking it and a setter keeping the value look the same, so the refusal names neither. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
33722
33722
  },
33723
33723
  value: {
33724
33724
  ops: ["set"],
@@ -33728,12 +33728,18 @@ var TOOL_SPECS = [
33728
33728
  method: {
33729
33729
  type: "string",
33730
33730
  ops: ["call"],
33731
- description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method taking no arguments returned, "get_viewport():gui_get_focus_owner".'
33731
+ description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a reading method returned, "get_viewport():gui_get_focus_owner", as runtime_inspect property describes.'
33732
33732
  },
33733
33733
  args: {
33734
33734
  type: "array",
33735
33735
  ops: ["call"],
33736
- description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
33736
+ description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
33737
+ },
33738
+ properties: {
33739
+ type: ["boolean", "array"],
33740
+ items: { type: "string" },
33741
+ ops: ["call"],
33742
+ description: 'call: read the object the call answered with in the same answer, under result_properties: true for every variable its script declares, or a list of names, each a colon path walked from that object the way property paths are ("dead_allowed", "terms:0:name"). A name that is not there is listed under result_properties_missing with why, and a result that is not an object answers properties_note instead. Without it an object comes back as its class and id only.'
33737
33743
  },
33738
33744
  timeoutMs: {
33739
33745
  type: "integer",
@@ -33815,7 +33821,7 @@ var TOOL_SPECS = [
33815
33821
  },
33816
33822
  {
33817
33823
  name: "runtime_input",
33818
- description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: the pointer ops answer with pointer_note there, because get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
33824
+ description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
33819
33825
  parameters: {
33820
33826
  projectPath: RUNNING_PROJECT_PATH,
33821
33827
  pid: RUNNING_PID,
@@ -33827,7 +33833,7 @@ var TOOL_SPECS = [
33827
33833
  says: {
33828
33834
  type: "string",
33829
33835
  ops: ["click"],
33830
- description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
33836
+ description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one. A match saying the words only as part of more is named under found.partOf, and is refused when a control saying exactly the words is hidden, off the screen or covered, since it would stand in for that one; index presses it anyway. An empty field says its placeholder. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
33831
33837
  },
33832
33838
  action: {
33833
33839
  type: "string",
@@ -33904,7 +33910,7 @@ var TOOL_SPECS = [
33904
33910
  requires: [],
33905
33911
  operations: {
33906
33912
  click: {
33907
- summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room",
33913
+ summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and link_pressed names the meta the label reports clicked. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
33908
33914
  requires: []
33909
33915
  },
33910
33916
  choose: {
@@ -34411,10 +34417,13 @@ function runtimeVerdict(endpoint, after) {
34411
34417
  return {
34412
34418
  listening: false,
34413
34419
  mayYetAnnounce: after.running,
34414
- note: after.running ? `nothing announced itself within ${after.budgetMs}ms${sized} and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms${sized} and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
34420
+ note: after.running ? `nothing announced itself within ${after.budgetMs}ms${sized} and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}. ${untilItAnnounces(after.servesAProject)}` : `nothing announced itself within ${after.budgetMs}ms${sized} and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
34415
34421
  heldAt: null
34416
34422
  };
34417
34423
  }
34424
+ function untilItAnnounces(servesAProject) {
34425
+ return servesAProject ? "Until it does, a runtime_* call for it waits briefly and is then refused, and another project's game answers only a call whose projectPath names that project." : "Until it does, a runtime_* call naming no projectPath reaches whichever game is the only one announced, another project's included, so pass projectPath to be sure the answer is this game's.";
34426
+ }
34418
34427
  function isOfType(value, kind) {
34419
34428
  switch (kind) {
34420
34429
  case "string":
@@ -34615,18 +34624,29 @@ function settingsNamedBy(op, args) {
34615
34624
  }
34616
34625
  var STALLED_AFTER_MS = 30000;
34617
34626
  var DEPENDED_FAILED = "Failed to compile depended scripts";
34627
+ function foldedScriptErrors(errors) {
34628
+ const scriptErrors = errors.filter((error) => !error.message.includes(DEPENDED_FAILED));
34629
+ const own = new Set(scriptErrors.map((error) => error.path));
34630
+ return {
34631
+ scriptErrors,
34632
+ failedThroughDependency: [...new Set(errors.map((error) => error.path))].filter((path) => !own.has(path))
34633
+ };
34634
+ }
34635
+ function nonEmpty(lists) {
34636
+ return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
34637
+ }
34618
34638
  function scriptErrorsNote(errors) {
34639
+ const { scriptErrors: own, failedThroughDependency: dependents } = foldedScriptErrors(errors);
34619
34640
  const causes = new Map;
34620
- for (const error of errors) {
34621
- if (!error.message.includes(DEPENDED_FAILED) && !causes.has(error.path)) {
34641
+ for (const error of own) {
34642
+ if (!causes.has(error.path)) {
34622
34643
  causes.set(error.path, error);
34623
34644
  }
34624
34645
  }
34625
- const dependents = [...new Set(errors.map((error) => error.path))].filter((path) => !causes.has(path));
34626
34646
  const scripts = causes.size + dependents.length;
34627
34647
  const named = [...causes.values()].map((error) => `${error.line === null ? error.path : `${error.path}:${error.line}`} ${error.message}${/[.!?]\)?$/.test(error.message) ? "" : "."}`).join(" ");
34628
34648
  const following = dependents.length === 0 ? "" : causes.size === 0 ? `${dependents.join(", ")} ${dependents.length === 1 ? "depends" : "depend"} on a script that did not compile, and gdUnit4 named none with an error of its own: script_diagnostics on one of them finds it.` : `${dependents.length === 1 ? "One more" : `${dependents.length} more`} failed only because a script ${dependents.length === 1 ? "it depends" : "they depend"} on did: ${dependents.join(", ")}.`;
34629
- const more = errors.filter((error) => !error.message.includes(DEPENDED_FAILED)).length - causes.size;
34649
+ const more = own.length - causes.size;
34630
34650
  const rest = more > 0 ? `${more === 1 ? "One more error" : `${more} more errors`} in ${causes.size === 1 ? "that script" : "those scripts"} ${more === 1 ? "is" : "are"} under scriptErrors.` : "";
34631
34651
  return [
34632
34652
  `No tests ran: gdUnit4 could not load ${scripts === 1 ? "one script" : `${scripts} scripts`} while looking for suites, and it runs no suite at all when one fails to load.`,
@@ -35491,7 +35511,8 @@ class GodotServer {
35491
35511
  ...whichGame(args),
35492
35512
  path: readNonEmptyString(args, "nodePath") ?? "",
35493
35513
  method: readString(args, "method") ?? "",
35494
- args: readArray(args, "args") ?? []
35514
+ args: readArray(args, "args") ?? [],
35515
+ ...args["properties"] === undefined ? {} : { properties: args["properties"] }
35495
35516
  }, patience);
35496
35517
  }
35497
35518
  case "runtime_capture":
@@ -36285,9 +36306,9 @@ class GodotServer {
36285
36306
  if (!paths.ok) {
36286
36307
  return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
36287
36308
  }
36288
- const asked = paths.value;
36289
- const missing = asked.filter((path) => !existsSync16(join20(project.value.path, path.slice("res://".length))));
36290
- if (missing.length > 0) {
36309
+ const missing = paths.value.filter((path) => !existsSync16(join20(project.value.path, path.slice("res://".length))));
36310
+ const asked = paths.value.filter((path) => !missing.includes(path));
36311
+ if (asked.length === 0) {
36291
36312
  const verdict = `nothing at ${missing.join(" and ")}`;
36292
36313
  return {
36293
36314
  content: [
@@ -36404,7 +36425,10 @@ class GodotServer {
36404
36425
  const hung = cutShort?.hung ?? false;
36405
36426
  const scriptErrors = timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
36406
36427
  const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
36407
- const verdict = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
36428
+ const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
36429
+ const nothingAt = missing.join(" and ");
36430
+ const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
36431
+ const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
36408
36432
  const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
36409
36433
  const marker = readNonEmptyString(args, "printed");
36410
36434
  const echoed = marker === undefined ? undefined : run.log.select({ severity: "info", sinceLastCall: false, contains: marker, limit: 200 });
@@ -36413,7 +36437,8 @@ class GodotServer {
36413
36437
  ...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
36414
36438
  };
36415
36439
  if (report === null) {
36416
- const note = nothingRan !== null ? `No tests ran: ${verdict}.` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${verdict}.` : `The test run wrote no report (${verdict}).`;
36440
+ const said = nothingRan !== null ? `No tests ran: ${ranAs}.` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${ranAs}.` : `The test run wrote no report (${ranAs}).`;
36441
+ const note = missingNote === undefined ? said : `${said} ${missingNote}`;
36417
36442
  return {
36418
36443
  content: [
36419
36444
  { type: "text", text: note },
@@ -36427,7 +36452,8 @@ class GodotServer {
36427
36452
  exitSignal: run.exitSignal ?? undefined,
36428
36453
  hung,
36429
36454
  ...timedOut ? { timedOut, silentForMs } : {},
36430
- ...scriptErrors.length > 0 ? { scriptErrors } : {},
36455
+ ...missing.length > 0 ? { missing } : {},
36456
+ ...nonEmpty(foldedScriptErrors(scriptErrors)),
36431
36457
  arguments: cmdArgs,
36432
36458
  entries: forAnswer(printed.slice(0, 60).map(aboveTheRunner)),
36433
36459
  ...printedLines,
@@ -36464,8 +36490,9 @@ class GodotServer {
36464
36490
  });
36465
36491
  const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
36466
36492
  return this.jsonTextResponse({
36467
- passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0,
36493
+ passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
36468
36494
  verdict,
36495
+ ...missing.length > 0 ? { missing } : {},
36469
36496
  ...timedOut ? { timedOut, hung, silentForMs } : {},
36470
36497
  exitCode,
36471
36498
  exitSignal: run.exitSignal ?? undefined,
@@ -36482,7 +36509,10 @@ class GodotServer {
36482
36509
  ...printedLines,
36483
36510
  savesNote,
36484
36511
  ...scanned,
36485
- note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true),
36512
+ note: [
36513
+ missingNote,
36514
+ ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
36515
+ ].filter((part) => part !== undefined).join(" ") || undefined,
36486
36516
  suites: unclean.map((suite) => ({
36487
36517
  name: suite.name,
36488
36518
  path: suite.path,
@@ -37256,7 +37286,14 @@ class GodotServer {
37256
37286
  async runtimeUp(projectPath, before, budget, withArgs) {
37257
37287
  const budgetMs = budget.ms;
37258
37288
  if (!existsSync16(join20(projectPath, RUNTIME_AUTOLOAD.path))) {
37259
- return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
37289
+ return runtimeVerdict(null, {
37290
+ addon: false,
37291
+ budgetMs,
37292
+ heldAt: null,
37293
+ running: false,
37294
+ withArgs,
37295
+ servesAProject: this.ownProject !== null
37296
+ });
37260
37297
  }
37261
37298
  let asking = null;
37262
37299
  let editorAskedAt = 0;
@@ -37308,6 +37345,7 @@ class GodotServer {
37308
37345
  heldAt: this.dapClient?.whereItStopped() ?? null,
37309
37346
  running: endpoint !== null || going !== null && await this.runStillGoing(going),
37310
37347
  withArgs,
37348
+ servesAProject: this.ownProject !== null,
37311
37349
  ...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
37312
37350
  });
37313
37351
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.44",
3
+ "version": "1.1.46",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",