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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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",
|