gdharness 0.6.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -15388,7 +15388,7 @@ var init_tool_definitions = __esm(() => {
15388
15388
  },
15389
15389
  {
15390
15390
  name: "project_test",
15391
- description: "Runs the project's gdUnit4 tests headless and answers with every case: which failed, where, and what the assertion said. The class list is rebuilt first, so a suite written a moment ago is found. 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. Needs gdUnit4 under addons/gdUnit4.",
15391
+ 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. 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. Needs gdUnit4 under addons/gdUnit4.",
15392
15392
  parameters: {
15393
15393
  projectPath: PROJECT_PATH,
15394
15394
  path: {
@@ -15891,6 +15891,11 @@ var init_tool_definitions = __esm(() => {
15891
15891
  ops: ["text", "choose"],
15892
15892
  description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
15893
15893
  },
15894
+ replace: {
15895
+ type: "boolean",
15896
+ ops: ["text"],
15897
+ description: 'text: true writes over what the field already says, which is what filling one in means. Default false types at the caret, so a field reading 2.1 typed "0.3" at reads 2.10.3. The answer says what the field holds afterwards either way.'
15898
+ },
15894
15899
  index: {
15895
15900
  type: "number",
15896
15901
  ops: ["choose"],
@@ -35976,6 +35981,17 @@ function realPathOr(path) {
35976
35981
  return path;
35977
35982
  }
35978
35983
  }
35984
+ function aboveTheRunner(entry) {
35985
+ const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
35986
+ if (runner < 0) {
35987
+ return entry;
35988
+ }
35989
+ const cut = entry.detail.length - runner;
35990
+ return {
35991
+ ...entry,
35992
+ detail: [...entry.detail.slice(0, runner), `[and ${cut} frames inside ${RUNNER_DIRECTORY}]`]
35993
+ };
35994
+ }
35979
35995
  function hasMainScene(projectFile) {
35980
35996
  const scene = parseProjectGodot(readFileSync12(projectFile, "utf8"))["application"]?.["run/main_scene"];
35981
35997
  return typeof scene === "string" && scene !== "";
@@ -36878,7 +36894,7 @@ class GodotServer {
36878
36894
  rmSync8(reportsDir, { recursive: true, force: true });
36879
36895
  rmSync8(userData, { recursive: true, force: true });
36880
36896
  }
36881
- const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
36897
+ const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
36882
36898
  const verdicts = {
36883
36899
  0: "passed",
36884
36900
  100: "failures",
@@ -36910,6 +36926,7 @@ class GodotServer {
36910
36926
  };
36911
36927
  }
36912
36928
  const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
36929
+ const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0);
36913
36930
  return this.jsonTextResponse({
36914
36931
  passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
36915
36932
  verdict,
@@ -36920,7 +36937,7 @@ class GodotServer {
36920
36937
  skipped: report.skipped,
36921
36938
  time: report.time,
36922
36939
  failed,
36923
- suites: report.suites.map((suite) => ({
36940
+ suites: unclean.map((suite) => ({
36924
36941
  name: suite.name,
36925
36942
  path: suite.path,
36926
36943
  tests: suite.tests,
@@ -36929,6 +36946,7 @@ class GodotServer {
36929
36946
  skipped: suite.skipped,
36930
36947
  time: suite.time
36931
36948
  })),
36949
+ suitesPassed: report.suites.length - unclean.length,
36932
36950
  engineErrors: run.log.count("error"),
36933
36951
  engineWarnings: run.log.count("warning"),
36934
36952
  engineEntries,
@@ -37520,7 +37538,7 @@ async function runGodotServer() {
37520
37538
  const server = new GodotServer;
37521
37539
  await server.run();
37522
37540
  }
37523
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
37541
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS, RUNNER_DIRECTORY = "addons/gdUnit4/";
37524
37542
  var init_server3 = __esm(() => {
37525
37543
  init_mcp();
37526
37544
  init_stdio2();
@@ -38890,7 +38908,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
38890
38908
  | Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
38891
38909
  | What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
38892
38910
  | Press a button | \`runtime_input click\`, which says what was under the pointer |
38893
- | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\`; a submitted field has to be clicked again |
38911
+ | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
38894
38912
  | Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
38895
38913
  | Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
38896
38914
  | Answer a dialog, or press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
@@ -155,6 +155,11 @@ func inject_key(params: Dictionary) -> Dictionary:
155
155
  ## Pushed into the viewport for the reason [method click] is, and it matters more here: the focus
156
156
  ## is what decides where a character lands, so a caller that clicked a field and then typed would
157
157
  ## otherwise have both waiting in the same queue with nothing said about the order.
158
+ ##
159
+ ## [code]replace[/code] is for a field that already says something, which is most of them: typing
160
+ ## lands at the caret, so a spin box reading 2.1 typed "0.3" at reads 2.10.3 and parses back to
161
+ ## 2.1, and the answer said four characters had gone in. What the field holds afterwards is read
162
+ ## back now rather than echoed from the request, so a call that typed somewhere unhelpful says so.
158
163
  func inject_text(params: Dictionary) -> Dictionary:
159
164
  var text: String = String(params.get("text", ""))
160
165
  if text.is_empty():
@@ -166,6 +171,7 @@ func inject_text(params: Dictionary) -> Dictionary:
166
171
  if not shut.is_empty():
167
172
  return {"type": "error", "message": shut}
168
173
 
174
+ var replaced: bool = bool(params.get("replace", false)) and _select_everything(focused)
169
175
  for index: int in text.length():
170
176
  var down: InputEventKey = _typed(text.unicode_at(index))
171
177
  viewport.push_input(down)
@@ -186,9 +192,44 @@ func inject_text(params: Dictionary) -> Dictionary:
186
192
  "text": text,
187
193
  "characters": text.length(),
188
194
  "into": into,
195
+ "replaced": replaced,
196
+ "holds": _what_it_holds(focused),
189
197
  }
190
198
 
191
199
 
200
+ ## Selects everything in [param focused] so the next character typed writes over it, and answers
201
+ ## whether there was a field to select in.
202
+ ##
203
+ ## Through the control rather than through a Ctrl+A, which is the one part of filling a field that
204
+ ## cannot honestly be sent as a key: the shortcut is Cmd+A on macOS and is an [InputMap] action a
205
+ ## project is free to unbind, and a caller asking for the field to be replaced would then be typing
206
+ ## on the end of what was there. The replacement itself is still every character a player types.
207
+ static func _select_everything(focused: Control) -> bool:
208
+ var field: LineEdit = focused as LineEdit
209
+ if field != null:
210
+ field.select_all()
211
+ return true
212
+ var box: TextEdit = focused as TextEdit
213
+ if box == null:
214
+ return false
215
+ box.select_all()
216
+ return true
217
+
218
+
219
+ ## What the field says now, or null where the focus is not a field at all.
220
+ ##
221
+ ## A spin box rewrites its own text out of the number it parsed, so this is the only thing that
222
+ ## says whether what was typed became what the field means.
223
+ static func _what_it_holds(focused: Control) -> Variant:
224
+ var field: LineEdit = focused as LineEdit
225
+ if field != null:
226
+ return field.text
227
+ var box: TextEdit = focused as TextEdit
228
+ if box == null:
229
+ return null
230
+ return box.text
231
+
232
+
192
233
  ## Why nothing typed would reach [param focused], or "" when it would.
193
234
  ##
194
235
  ## Having the focus is not the same as being edited. Since Godot 4.4 a field is focused and shut
package/build/index.js CHANGED
@@ -27941,7 +27941,7 @@ var TOOL_SPECS = [
27941
27941
  },
27942
27942
  {
27943
27943
  name: "project_test",
27944
- description: "Runs the project's gdUnit4 tests headless and answers with every case: which failed, where, and what the assertion said. The class list is rebuilt first, so a suite written a moment ago is found. 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. Needs gdUnit4 under addons/gdUnit4.",
27944
+ 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. 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. Needs gdUnit4 under addons/gdUnit4.",
27945
27945
  parameters: {
27946
27946
  projectPath: PROJECT_PATH,
27947
27947
  path: {
@@ -28444,6 +28444,11 @@ var TOOL_SPECS = [
28444
28444
  ops: ["text", "choose"],
28445
28445
  description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
28446
28446
  },
28447
+ replace: {
28448
+ type: "boolean",
28449
+ ops: ["text"],
28450
+ description: 'text: true writes over what the field already says, which is what filling one in means. Default false types at the caret, so a field reading 2.1 typed "0.3" at reads 2.10.3. The answer says what the field holds afterwards either way.'
28451
+ },
28447
28452
  index: {
28448
28453
  type: "number",
28449
28454
  ops: ["choose"],
@@ -28756,6 +28761,18 @@ function realPathOr(path) {
28756
28761
  return path;
28757
28762
  }
28758
28763
  }
28764
+ var RUNNER_DIRECTORY = "addons/gdUnit4/";
28765
+ function aboveTheRunner(entry) {
28766
+ const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
28767
+ if (runner < 0) {
28768
+ return entry;
28769
+ }
28770
+ const cut = entry.detail.length - runner;
28771
+ return {
28772
+ ...entry,
28773
+ detail: [...entry.detail.slice(0, runner), `[and ${cut} frames inside ${RUNNER_DIRECTORY}]`]
28774
+ };
28775
+ }
28759
28776
  function hasMainScene(projectFile) {
28760
28777
  const scene = parseProjectGodot(readFileSync8(projectFile, "utf8"))["application"]?.["run/main_scene"];
28761
28778
  return typeof scene === "string" && scene !== "";
@@ -29658,7 +29675,7 @@ class GodotServer {
29658
29675
  rmSync4(reportsDir, { recursive: true, force: true });
29659
29676
  rmSync4(userData, { recursive: true, force: true });
29660
29677
  }
29661
- const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
29678
+ const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
29662
29679
  const verdicts = {
29663
29680
  0: "passed",
29664
29681
  100: "failures",
@@ -29690,6 +29707,7 @@ class GodotServer {
29690
29707
  };
29691
29708
  }
29692
29709
  const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
29710
+ const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0);
29693
29711
  return this.jsonTextResponse({
29694
29712
  passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
29695
29713
  verdict,
@@ -29700,7 +29718,7 @@ class GodotServer {
29700
29718
  skipped: report.skipped,
29701
29719
  time: report.time,
29702
29720
  failed,
29703
- suites: report.suites.map((suite) => ({
29721
+ suites: unclean.map((suite) => ({
29704
29722
  name: suite.name,
29705
29723
  path: suite.path,
29706
29724
  tests: suite.tests,
@@ -29709,6 +29727,7 @@ class GodotServer {
29709
29727
  skipped: suite.skipped,
29710
29728
  time: suite.time
29711
29729
  })),
29730
+ suitesPassed: report.suites.length - unclean.length,
29712
29731
  engineErrors: run.log.count("error"),
29713
29732
  engineWarnings: run.log.count("warning"),
29714
29733
  engineEntries,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.6.9",
3
+ "version": "0.7.0",
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",