gdharness 1.1.9 → 1.1.10
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 +928 -1251
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +21 -2
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +6 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +113 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd.uid +1 -0
- package/build/index.js +94 -157
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ const Values = preload("runtime_values.gd")
|
|
|
10
10
|
## reports are the same place by construction rather than by agreement.
|
|
11
11
|
const Queries = preload("runtime_queries.gd")
|
|
12
12
|
const Read = preload("reading.gd")
|
|
13
|
+
const Targets = preload("runtime_targets.gd")
|
|
13
14
|
const Words = preload("runtime_words.gd")
|
|
14
15
|
|
|
15
16
|
## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
|
|
@@ -484,10 +485,25 @@ static func _scroll_into_view(control: Control) -> bool:
|
|
|
484
485
|
## A control out of sight inside a ScrollContainer is scrolled to first; the answer says so under
|
|
485
486
|
## `scrolled_into_view`, because the view having moved is a thing that happened to the screen and
|
|
486
487
|
## the caller is the only one who can tell whether that matters.
|
|
488
|
+
##
|
|
489
|
+
## `says` names the control by the words on it instead, under `path` when that is given as well,
|
|
490
|
+
## found and pressed in the same frame. A find and then a click by path was two round trips over a
|
|
491
|
+
## generated path copied whole, and a panel that rebuilt itself between them freed the button, so
|
|
492
|
+
## the click landed on nothing.
|
|
487
493
|
func click(params: Dictionary) -> Dictionary:
|
|
488
494
|
var node_path: String = str(params.get("path", ""))
|
|
495
|
+
var wanted: String = str(params.get("says", ""))
|
|
496
|
+
var found: Variant = null
|
|
497
|
+
if not wanted.is_empty():
|
|
498
|
+
var picked: Dictionary = Targets.control_saying(
|
|
499
|
+
_host.get_tree().root, "/root" if node_path.is_empty() else node_path, wanted, params.get("index")
|
|
500
|
+
)
|
|
501
|
+
if picked.has("message"):
|
|
502
|
+
return picked
|
|
503
|
+
node_path = picked["path"]
|
|
504
|
+
found = picked["found"]
|
|
489
505
|
if node_path.is_empty():
|
|
490
|
-
return {"type": "error", "message": "
|
|
506
|
+
return {"type": "error", "message": "click needs a path, or says to find the control by its words"}
|
|
491
507
|
|
|
492
508
|
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
493
509
|
if standing.has("message"):
|
|
@@ -582,7 +598,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
582
598
|
if is_instance_valid(control):
|
|
583
599
|
afterwards = "in_tree" if control.is_inside_tree() else "removed"
|
|
584
600
|
|
|
585
|
-
|
|
601
|
+
var answer: Dictionary = {
|
|
586
602
|
"type": "clicked",
|
|
587
603
|
"path": node_path,
|
|
588
604
|
"position": _values.serialize(position),
|
|
@@ -593,6 +609,9 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
593
609
|
"control_afterwards": afterwards,
|
|
594
610
|
"scrolled_into_view": scrolled,
|
|
595
611
|
}
|
|
612
|
+
if found != null:
|
|
613
|
+
answer["found"] = found
|
|
614
|
+
return answer
|
|
596
615
|
|
|
597
616
|
|
|
598
617
|
## What to add to a refusal about a point outside the viewport, when the reason is that nobody
|
|
@@ -259,6 +259,12 @@ static func _says(said: String, wanted: String) -> bool:
|
|
|
259
259
|
return said.containsn(words) if _is_literal(words) else said.matchn(words)
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
## Whether [param node]'s own words are what [param wanted] asks for, by the rules a find uses, so a
|
|
263
|
+
## click that names its control by its words finds the one a find would have answered with.
|
|
264
|
+
static func says(node: Node, wanted: String) -> bool:
|
|
265
|
+
return _says(Words.said_by(node), wanted)
|
|
266
|
+
|
|
267
|
+
|
|
262
268
|
## [param wanted], a glob, open at both ends so it matches its words anywhere in a text; "" when
|
|
263
269
|
## it is not a glob, or already open at both ends, and so has nothing to suggest.
|
|
264
270
|
static func _widened(wanted: String) -> String:
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Which control a click means when it names the control by the words on it rather than by path.
|
|
4
|
+
|
|
5
|
+
const Queries = preload("runtime_queries.gd")
|
|
6
|
+
const Read = preload("reading.gd")
|
|
7
|
+
const Values = preload("runtime_values.gd")
|
|
8
|
+
const Words = preload("runtime_words.gd")
|
|
9
|
+
|
|
10
|
+
## How many of the controls saying the same words a refusal names, with the index for each.
|
|
11
|
+
const LISTED_CANDIDATES: int = 10
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## The control a click by [param wanted] words presses, found under [param root_path] in the tree
|
|
15
|
+
## [param root] heads, or the refusal saying why there is none or which of several:
|
|
16
|
+
## {"path", "found"} or {"type", "message"}.
|
|
17
|
+
##
|
|
18
|
+
## Matched the way a find matches `says`, against a control's own words, and only among controls
|
|
19
|
+
## the player can see. A match inside a button presses the button, because the label is what says
|
|
20
|
+
## the words and the button is what takes the click: aimed at the label, the pointer lands on the
|
|
21
|
+
## button and the answer would call the click a miss. Several matches are refused rather than
|
|
22
|
+
## guessed, each named with the [param which] that picks it, counting from 0.
|
|
23
|
+
static func control_saying(root: Node, root_path: String, wanted: String, which: Variant) -> Dictionary:
|
|
24
|
+
var reached: Dictionary = Values.node_at(root, root_path)
|
|
25
|
+
if reached.has("message"):
|
|
26
|
+
return reached
|
|
27
|
+
var targets: Array[Control] = []
|
|
28
|
+
# The words of the control that matched, which a button pressed on a label's behalf has none of.
|
|
29
|
+
var said: Array[String] = []
|
|
30
|
+
var hidden: int = 0
|
|
31
|
+
var pending: Array[Node] = [reached["node"]]
|
|
32
|
+
while not pending.is_empty():
|
|
33
|
+
var node: Node = pending.pop_front()
|
|
34
|
+
if node is Control and Queries.says(node, wanted):
|
|
35
|
+
if not Queries.shown(node):
|
|
36
|
+
hidden += 1
|
|
37
|
+
else:
|
|
38
|
+
var matched: Control = node
|
|
39
|
+
var target: Control = _pressed_through(matched)
|
|
40
|
+
if not targets.has(target):
|
|
41
|
+
targets.append(target)
|
|
42
|
+
said.append(Words.said_by(node))
|
|
43
|
+
var children: Array[Node] = node.get_children(true)
|
|
44
|
+
for index: int in range(children.size() - 1, -1, -1):
|
|
45
|
+
pending.push_front(children[index])
|
|
46
|
+
|
|
47
|
+
var under: String = "" if root_path == "/root" else " under %s" % root_path
|
|
48
|
+
if targets.is_empty():
|
|
49
|
+
return {
|
|
50
|
+
"type": "error",
|
|
51
|
+
"message":
|
|
52
|
+
(
|
|
53
|
+
'no control on screen%s says "%s", so nothing was clicked%s'
|
|
54
|
+
% [under, wanted, _hidden_note(hidden)]
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if which == null and targets.size() > 1:
|
|
58
|
+
return {
|
|
59
|
+
"type": "error",
|
|
60
|
+
"message":
|
|
61
|
+
(
|
|
62
|
+
'%d controls on screen%s say "%s", so which to click is not clear; pass index, counting from 0: %s'
|
|
63
|
+
% [targets.size(), under, wanted, _candidates(targets, said)]
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
var index: int = 0 if which == null else Read.as_int(which, -1)
|
|
67
|
+
if index < 0 or index >= targets.size():
|
|
68
|
+
var plural: String = "" if targets.size() == 1 else "s"
|
|
69
|
+
return {
|
|
70
|
+
"type": "error",
|
|
71
|
+
"message":
|
|
72
|
+
(
|
|
73
|
+
'index %s is not one of the %d control%s on screen%s saying "%s": %s'
|
|
74
|
+
% [str(which), targets.size(), plural, under, wanted, _candidates(targets, said)]
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
"path": str(targets[index].get_path()),
|
|
79
|
+
"found": {"says": wanted, "index": index, "of": targets.size()},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## What to add to a refusal that found nothing on screen, when words matched controls out of sight.
|
|
84
|
+
static func _hidden_note(hidden: int) -> String:
|
|
85
|
+
if hidden == 0:
|
|
86
|
+
return ""
|
|
87
|
+
return "; %d hidden control%s" % [hidden, " says it" if hidden == 1 else "s say it"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## [param control], or the button holding it when it is not one itself, which is what a click on
|
|
91
|
+
## its words means.
|
|
92
|
+
static func _pressed_through(control: Control) -> Control:
|
|
93
|
+
var walk: Node = control
|
|
94
|
+
while walk != null:
|
|
95
|
+
if walk is BaseButton:
|
|
96
|
+
var pressed: BaseButton = walk
|
|
97
|
+
return pressed
|
|
98
|
+
walk = walk.get_parent()
|
|
99
|
+
return control
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## The controls a click by words could mean, each with its index, its path and the words that
|
|
103
|
+
## matched, [param said] holding those in the same order.
|
|
104
|
+
static func _candidates(targets: Array[Control], said: Array[String]) -> String:
|
|
105
|
+
var named: Array[String] = []
|
|
106
|
+
for index: int in mini(targets.size(), LISTED_CANDIDATES):
|
|
107
|
+
var words: String = said[index].replace("\n", " ")
|
|
108
|
+
if words.length() > 60:
|
|
109
|
+
words = words.substr(0, 57) + "..."
|
|
110
|
+
named.append('%d %s ("%s")' % [index, targets[index].get_path(), words])
|
|
111
|
+
if targets.size() > LISTED_CANDIDATES:
|
|
112
|
+
named.append("and %d more" % (targets.size() - LISTED_CANDIDATES))
|
|
113
|
+
return "; ".join(named)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://be1ydcqg2ag4t
|
package/build/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @bun
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
3
|
var __create = Object.create;
|
|
5
4
|
var __getProtoOf = Object.getPrototypeOf;
|
|
@@ -10085,6 +10084,14 @@ function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDi
|
|
|
10085
10084
|
// src/issues.ts
|
|
10086
10085
|
var NEW_ISSUE = "https://github.com/Aureliolo/gdharness/issues/new";
|
|
10087
10086
|
var ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
|
|
10087
|
+
function feedbackNotice() {
|
|
10088
|
+
return JSON.stringify({
|
|
10089
|
+
gdharness_feedback: {
|
|
10090
|
+
what_to_do: "If this session has run into something gdharness cannot do, or a tool that should " + "exist and does not, tell the user what is missing and offer to open an enhancement " + "issue for them. Only if they say yes, and only for a real gap: a call you got wrong " + "is not one. If you cannot open an issue yourself, give them the link and what to " + "write in it.",
|
|
10091
|
+
enhancement_url: ENHANCEMENT_URL
|
|
10092
|
+
}
|
|
10093
|
+
}, null, 2);
|
|
10094
|
+
}
|
|
10088
10095
|
function runtime() {
|
|
10089
10096
|
const versions = process3.versions;
|
|
10090
10097
|
const bun = versions["bun"];
|
|
@@ -10140,6 +10147,16 @@ function filledTemplate(where, message, godotVersion) {
|
|
|
10140
10147
|
].join(`
|
|
10141
10148
|
`);
|
|
10142
10149
|
}
|
|
10150
|
+
|
|
10151
|
+
class DefectsSeen {
|
|
10152
|
+
counts = new Map;
|
|
10153
|
+
before(where, message) {
|
|
10154
|
+
const signature = defectSignature(where, message);
|
|
10155
|
+
const seen = this.counts.get(signature) ?? 0;
|
|
10156
|
+
this.counts.set(signature, seen + 1);
|
|
10157
|
+
return seen;
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10143
10160
|
function defectReport(where, error, context = {}) {
|
|
10144
10161
|
const { godotVersion, seenBefore = 0 } = context;
|
|
10145
10162
|
const message = errorMessage(error);
|
|
@@ -26896,19 +26913,21 @@ class GameLog {
|
|
|
26896
26913
|
this.seen[severity] = this.entries.length;
|
|
26897
26914
|
}
|
|
26898
26915
|
}
|
|
26899
|
-
const matching = this.entries.slice(floor).filter((entry) =>
|
|
26900
|
-
if (!wanted.has(entry.severity)) {
|
|
26901
|
-
return false;
|
|
26902
|
-
}
|
|
26903
|
-
if (needle === undefined) {
|
|
26904
|
-
return true;
|
|
26905
|
-
}
|
|
26906
|
-
return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
|
|
26907
|
-
});
|
|
26916
|
+
const matching = this.entries.slice(floor).filter((entry) => answersTo(entry, options.severity, options.contains));
|
|
26908
26917
|
const omitted = Math.max(0, matching.length - options.limit);
|
|
26909
26918
|
return { entries: matching.slice(omitted), omitted };
|
|
26910
26919
|
}
|
|
26911
26920
|
}
|
|
26921
|
+
function answersTo(entry, severity, contains) {
|
|
26922
|
+
if (!ADMITTED[severity].has(entry.severity)) {
|
|
26923
|
+
return false;
|
|
26924
|
+
}
|
|
26925
|
+
if (contains === undefined) {
|
|
26926
|
+
return true;
|
|
26927
|
+
}
|
|
26928
|
+
const needle = contains.toLowerCase();
|
|
26929
|
+
return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
|
|
26930
|
+
}
|
|
26912
26931
|
|
|
26913
26932
|
// src/editor-log.ts
|
|
26914
26933
|
function editorLogPath(projectPath) {
|
|
@@ -27018,7 +27037,7 @@ function shapeOf(text) {
|
|
|
27018
27037
|
}
|
|
27019
27038
|
return { shape, values };
|
|
27020
27039
|
}
|
|
27021
|
-
function bursts(entries, before) {
|
|
27040
|
+
function bursts(entries, before, keep) {
|
|
27022
27041
|
const shapes = new Map;
|
|
27023
27042
|
const shaped = entries.map((entry) => shapeOf(entry.text));
|
|
27024
27043
|
for (const [at, entry] of entries.entries()) {
|
|
@@ -27046,6 +27065,9 @@ function bursts(entries, before) {
|
|
|
27046
27065
|
if (entry === undefined) {
|
|
27047
27066
|
continue;
|
|
27048
27067
|
}
|
|
27068
|
+
if (keep !== undefined && !group.indices.some((at) => entries[at] !== undefined && keep(entries[at]))) {
|
|
27069
|
+
continue;
|
|
27070
|
+
}
|
|
27049
27071
|
repeated.push({
|
|
27050
27072
|
shape: key.slice(key.indexOf("\x00") + 1),
|
|
27051
27073
|
count: group.indices.length,
|
|
@@ -27968,126 +27990,6 @@ var EDITOR_READS = dictionary({
|
|
|
27968
27990
|
project_settings: dictionary({ get: "get_project_setting" })
|
|
27969
27991
|
});
|
|
27970
27992
|
|
|
27971
|
-
// src/issues.ts
|
|
27972
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
27973
|
-
import process5 from "node:process";
|
|
27974
|
-
var NEW_ISSUE2 = "https://github.com/Aureliolo/gdharness/issues/new";
|
|
27975
|
-
var ENHANCEMENT_URL2 = `${NEW_ISSUE2}?template=feature_request.md`;
|
|
27976
|
-
function feedbackNotice() {
|
|
27977
|
-
return JSON.stringify({
|
|
27978
|
-
gdharness_feedback: {
|
|
27979
|
-
what_to_do: "If this session has run into something gdharness cannot do, or a tool that should " + "exist and does not, tell the user what is missing and offer to open an enhancement " + "issue for them. Only if they say yes, and only for a real gap: a call you got wrong " + "is not one. If you cannot open an issue yourself, give them the link and what to " + "write in it.",
|
|
27980
|
-
enhancement_url: ENHANCEMENT_URL2
|
|
27981
|
-
}
|
|
27982
|
-
}, null, 2);
|
|
27983
|
-
}
|
|
27984
|
-
function runtime2() {
|
|
27985
|
-
const versions = process5.versions;
|
|
27986
|
-
const bun = versions["bun"];
|
|
27987
|
-
return bun === undefined ? `node ${process5.versions.node}` : `bun ${bun}`;
|
|
27988
|
-
}
|
|
27989
|
-
function defectSignature2(where, message) {
|
|
27990
|
-
const normalised = `${where}
|
|
27991
|
-
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
27992
|
-
return createHash2("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
27993
|
-
}
|
|
27994
|
-
function facts2(where, message, godotVersion) {
|
|
27995
|
-
return [
|
|
27996
|
-
["Where", where],
|
|
27997
|
-
["Error", message],
|
|
27998
|
-
["Version", `gdharness ${SERVER_VERSION}`],
|
|
27999
|
-
["Runtime", `${runtime2()}, ${process5.platform} ${process5.arch}`],
|
|
28000
|
-
["Godot", godotVersion ?? "not known at the point this failed"],
|
|
28001
|
-
["Signature", defectSignature2(where, message)]
|
|
28002
|
-
];
|
|
28003
|
-
}
|
|
28004
|
-
function filledTemplate2(where, message, godotVersion) {
|
|
28005
|
-
return [
|
|
28006
|
-
`**gdharness version**: ${SERVER_VERSION}`,
|
|
28007
|
-
`**Godot version**: ${godotVersion ?? ""}`,
|
|
28008
|
-
`**Bun version**: ${runtime2()}`,
|
|
28009
|
-
`**OS**: ${process5.platform} ${process5.arch}`,
|
|
28010
|
-
"**MCP client**:",
|
|
28011
|
-
"",
|
|
28012
|
-
"## What you did",
|
|
28013
|
-
"",
|
|
28014
|
-
where,
|
|
28015
|
-
"",
|
|
28016
|
-
"## What you expected",
|
|
28017
|
-
"",
|
|
28018
|
-
"The call to answer, or to refuse with a reason.",
|
|
28019
|
-
"",
|
|
28020
|
-
"## What happened",
|
|
28021
|
-
"",
|
|
28022
|
-
"gdharness failed in a way it does not model.",
|
|
28023
|
-
"",
|
|
28024
|
-
"```text",
|
|
28025
|
-
message,
|
|
28026
|
-
"```",
|
|
28027
|
-
"",
|
|
28028
|
-
`Signature: \`${defectSignature2(where, message)}\``,
|
|
28029
|
-
"",
|
|
28030
|
-
"If the real cause turned out to be the project, the environment or the call, say so here:",
|
|
28031
|
-
"reaching you as a defect rather than as a refusal naming what would have worked is then the",
|
|
28032
|
-
"thing to fix.",
|
|
28033
|
-
"",
|
|
28034
|
-
"## Anything that makes it reproducible",
|
|
28035
|
-
""
|
|
28036
|
-
].join(`
|
|
28037
|
-
`);
|
|
28038
|
-
}
|
|
28039
|
-
|
|
28040
|
-
class DefectsSeen {
|
|
28041
|
-
counts = new Map;
|
|
28042
|
-
before(where, message) {
|
|
28043
|
-
const signature = defectSignature2(where, message);
|
|
28044
|
-
const seen = this.counts.get(signature) ?? 0;
|
|
28045
|
-
this.counts.set(signature, seen + 1);
|
|
28046
|
-
return seen;
|
|
28047
|
-
}
|
|
28048
|
-
}
|
|
28049
|
-
function defectReport2(where, error, context = {}) {
|
|
28050
|
-
const { godotVersion, seenBefore = 0 } = context;
|
|
28051
|
-
const message = errorMessage(error);
|
|
28052
|
-
const title = `Defect ${defectSignature2(where, message)}: ${where}`;
|
|
28053
|
-
const url = `${NEW_ISSUE2}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate2(where, message, godotVersion))}`;
|
|
28054
|
-
const width = Math.max(...facts2(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
28055
|
-
return [
|
|
28056
|
-
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
28057
|
-
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
28058
|
-
"in gdharness however the rest of it turns out.",
|
|
28059
|
-
"",
|
|
28060
|
-
...seenBefore === 0 ? [
|
|
28061
|
-
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
28062
|
-
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
28063
|
-
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
28064
|
-
"further only spends turns."
|
|
28065
|
-
] : [
|
|
28066
|
-
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
28067
|
-
"state rather than a passing one, and sending the call again will not change it."
|
|
28068
|
-
],
|
|
28069
|
-
"",
|
|
28070
|
-
...facts2(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
28071
|
-
"",
|
|
28072
|
-
"Do not open an issue on your own initiative. Tell the person whose machine this is what",
|
|
28073
|
-
"broke, and ask whether you may report it to gdharness on their behalf. What the report",
|
|
28074
|
-
"carries is the lines above and nothing else, and whether that goes into a public tracker",
|
|
28075
|
-
"is theirs to decide.",
|
|
28076
|
-
"",
|
|
28077
|
-
"If they say yes, this link carries the report already filled in:",
|
|
28078
|
-
url,
|
|
28079
|
-
"",
|
|
28080
|
-
"If they say yes and you have no way to open an issue yourself, give them that link and",
|
|
28081
|
-
"those lines, and stay with them while they file it.",
|
|
28082
|
-
"",
|
|
28083
|
-
"Worth reporting either way. If you can see that what actually went wrong was the project,",
|
|
28084
|
-
"the environment or the call, then this message is the defect: a failure gdharness knows",
|
|
28085
|
-
"about is meant to arrive as a refusal naming what would have worked, not as this. Say that",
|
|
28086
|
-
"in the report and it is the more useful of the two."
|
|
28087
|
-
].join(`
|
|
28088
|
-
`);
|
|
28089
|
-
}
|
|
28090
|
-
|
|
28091
27993
|
// src/junit.ts
|
|
28092
27994
|
class MalformedReportError extends Error {
|
|
28093
27995
|
constructor(message) {
|
|
@@ -28290,7 +28192,7 @@ function scriptErrorsPrinted(printed) {
|
|
|
28290
28192
|
for (const line of printed.slice(start + 1)) {
|
|
28291
28193
|
const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
|
|
28292
28194
|
if (at !== null && message !== null) {
|
|
28293
|
-
found.push({ path: at[1] ?? "", line: Number(at[2]), message });
|
|
28195
|
+
found.push({ path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]), message });
|
|
28294
28196
|
message = null;
|
|
28295
28197
|
} else if (at === null && message === null && /^ {2}\S/.test(line)) {
|
|
28296
28198
|
message = line.trim();
|
|
@@ -28298,7 +28200,7 @@ function scriptErrorsPrinted(printed) {
|
|
|
28298
28200
|
break;
|
|
28299
28201
|
}
|
|
28300
28202
|
}
|
|
28301
|
-
return found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : a.line - b.line);
|
|
28203
|
+
return found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
|
|
28302
28204
|
}
|
|
28303
28205
|
var ESCAPE = String.fromCharCode(27);
|
|
28304
28206
|
var LOCATION = `'[^'\\n]*' in \\S+:\\d+`;
|
|
@@ -29193,7 +29095,7 @@ function searchProject(projectPath, options) {
|
|
|
29193
29095
|
}
|
|
29194
29096
|
|
|
29195
29097
|
// src/reimport.ts
|
|
29196
|
-
import { createHash as
|
|
29098
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
29197
29099
|
import { existsSync as existsSync9, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
29198
29100
|
import { basename as basename2, join as join13 } from "node:path";
|
|
29199
29101
|
function forceNextImport(projectPath, resourcePath) {
|
|
@@ -29208,7 +29110,7 @@ function forceNextImport(projectPath, resourcePath) {
|
|
|
29208
29110
|
rmSync5(importRecord(projectPath, resourcePath), { force: true });
|
|
29209
29111
|
}
|
|
29210
29112
|
function importRecord(projectPath, resourcePath) {
|
|
29211
|
-
const digest =
|
|
29113
|
+
const digest = createHash2("md5").update(resourcePath).digest("hex");
|
|
29212
29114
|
return join13(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
|
|
29213
29115
|
}
|
|
29214
29116
|
|
|
@@ -29494,7 +29396,7 @@ function restartSettled(projectPath) {
|
|
|
29494
29396
|
|
|
29495
29397
|
// src/run-record.ts
|
|
29496
29398
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
29497
|
-
import { createHash as
|
|
29399
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
29498
29400
|
import {
|
|
29499
29401
|
mkdirSync as mkdirSync8,
|
|
29500
29402
|
openSync as openSync2,
|
|
@@ -29528,7 +29430,7 @@ function realAsFarAsItExists(path) {
|
|
|
29528
29430
|
function projectKey(projectPath) {
|
|
29529
29431
|
const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
|
|
29530
29432
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
29531
|
-
return
|
|
29433
|
+
return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
29532
29434
|
}
|
|
29533
29435
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
29534
29436
|
return join15(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
@@ -29836,7 +29738,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29836
29738
|
}
|
|
29837
29739
|
|
|
29838
29740
|
// src/setup.ts
|
|
29839
|
-
import { createHash as
|
|
29741
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
29840
29742
|
import {
|
|
29841
29743
|
cpSync,
|
|
29842
29744
|
existsSync as existsSync10,
|
|
@@ -29859,7 +29761,7 @@ var RUNTIME_AUTOLOAD = {
|
|
|
29859
29761
|
var VERSION_MARKER = ".gdharness-version";
|
|
29860
29762
|
var DIGEST_MARKER = ".gdharness-digest";
|
|
29861
29763
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
29862
|
-
const hash =
|
|
29764
|
+
const hash = createHash4("sha256");
|
|
29863
29765
|
const walk = (directory, relative) => {
|
|
29864
29766
|
for (const name of readdirSync6(directory).sort()) {
|
|
29865
29767
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
@@ -30282,6 +30184,10 @@ var TOOL_SPECS = [
|
|
|
30282
30184
|
type: "integer",
|
|
30283
30185
|
minimum: 1,
|
|
30284
30186
|
description: "How long the run may take before it is killed. Default 600000. A run killed at the limit answers timedOut and silentForMs, how long it had printed nothing; it is called hung only when that was half the limit, or thirty seconds of a longer one, and otherwise it was still running and wants a longer timeoutMs or a narrower path."
|
|
30187
|
+
},
|
|
30188
|
+
printed: {
|
|
30189
|
+
type: "string",
|
|
30190
|
+
description: "Also answer with every line the run printed that contains this, under printed, whether it passed or not: a suite that prints a marker beside the numbers it measured carries them back without failing an assertion to do it. Matched case aside, in the text and the backtrace, at most 200 lines, the newest kept; printedOmitted counts the rest."
|
|
30285
30191
|
}
|
|
30286
30192
|
},
|
|
30287
30193
|
requires: ["projectPath"]
|
|
@@ -30931,14 +30837,19 @@ var TOOL_SPECS = [
|
|
|
30931
30837
|
},
|
|
30932
30838
|
{
|
|
30933
30839
|
name: "runtime_input",
|
|
30934
|
-
description: "Input to the running game: a whole click on a Control or a 3D node named by path, 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.",
|
|
30840
|
+
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.",
|
|
30935
30841
|
parameters: {
|
|
30936
30842
|
projectPath: RUNNING_PROJECT_PATH,
|
|
30937
30843
|
pid: RUNNING_PID,
|
|
30938
30844
|
nodePath: {
|
|
30939
30845
|
type: "string",
|
|
30940
30846
|
ops: ["click", "choose"],
|
|
30941
|
-
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
|
|
30847
|
+
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn; with says, where to look for the control instead, default the whole tree. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
|
|
30848
|
+
},
|
|
30849
|
+
says: {
|
|
30850
|
+
type: "string",
|
|
30851
|
+
ops: ["click"],
|
|
30852
|
+
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), 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. Several matches are refused with each one listed, and index picks one."
|
|
30942
30853
|
},
|
|
30943
30854
|
action: {
|
|
30944
30855
|
type: "string",
|
|
@@ -30975,8 +30886,8 @@ var TOOL_SPECS = [
|
|
|
30975
30886
|
},
|
|
30976
30887
|
index: {
|
|
30977
30888
|
type: "number",
|
|
30978
|
-
ops: ["choose"],
|
|
30979
|
-
description: "choose: the item to take, by where it is in the list, when text will not do."
|
|
30889
|
+
ops: ["click", "choose"],
|
|
30890
|
+
description: "click: which of several controls saying the words, counting from 0 in the order a refusal lists them. choose: the item to take, by where it is in the list, when text will not do."
|
|
30980
30891
|
},
|
|
30981
30892
|
shift: { type: "boolean", ops: ["key"], description: "key: hold Shift with it. Default false." },
|
|
30982
30893
|
ctrl: { type: "boolean", ops: ["key"], description: "key: hold Ctrl with it. Default false." },
|
|
@@ -31015,8 +30926,8 @@ var TOOL_SPECS = [
|
|
|
31015
30926
|
requires: [],
|
|
31016
30927
|
operations: {
|
|
31017
30928
|
click: {
|
|
31018
|
-
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. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press",
|
|
31019
|
-
requires: [
|
|
30929
|
+
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, and scrolled_into_view says whether the view moved. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press",
|
|
30930
|
+
requires: []
|
|
31020
30931
|
},
|
|
31021
30932
|
choose: {
|
|
31022
30933
|
summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and Enter presses it, which is the engine's own path and needs no window. Answers with what was chosen and what the button in front of it shows now",
|
|
@@ -31606,16 +31517,26 @@ function changedSettings(before, after) {
|
|
|
31606
31517
|
return [...names].filter((name) => JSON.stringify(before.get(name)) !== JSON.stringify(after.get(name))).sort();
|
|
31607
31518
|
}
|
|
31608
31519
|
var STALLED_AFTER_MS = 30000;
|
|
31520
|
+
var DEPENDED_FAILED = "Failed to compile depended scripts";
|
|
31609
31521
|
function scriptErrorsNote(errors) {
|
|
31610
|
-
const
|
|
31522
|
+
const causes = new Map;
|
|
31611
31523
|
for (const error of errors) {
|
|
31612
|
-
if (!
|
|
31613
|
-
|
|
31524
|
+
if (!error.message.includes(DEPENDED_FAILED) && !causes.has(error.path)) {
|
|
31525
|
+
causes.set(error.path, error);
|
|
31614
31526
|
}
|
|
31615
31527
|
}
|
|
31616
|
-
const
|
|
31617
|
-
const
|
|
31618
|
-
|
|
31528
|
+
const dependents = [...new Set(errors.map((error) => error.path))].filter((path) => !causes.has(path));
|
|
31529
|
+
const scripts = causes.size + dependents.length;
|
|
31530
|
+
const named = [...causes.values()].map((error) => `${error.line === null ? error.path : `${error.path}:${error.line}`} ${error.message}${/[.!?]\)?$/.test(error.message) ? "" : "."}`).join(" ");
|
|
31531
|
+
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(", ")}.`;
|
|
31532
|
+
const more = errors.filter((error) => !error.message.includes(DEPENDED_FAILED)).length - causes.size;
|
|
31533
|
+
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.` : "";
|
|
31534
|
+
return [
|
|
31535
|
+
`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.`,
|
|
31536
|
+
named,
|
|
31537
|
+
following,
|
|
31538
|
+
rest
|
|
31539
|
+
].filter((part) => part !== "").join(" ");
|
|
31619
31540
|
}
|
|
31620
31541
|
function timedOutVerdict(timeoutMs, silentForMs) {
|
|
31621
31542
|
const quiet = Math.round(silentForMs);
|
|
@@ -32049,7 +31970,7 @@ class GodotServer {
|
|
|
32049
31970
|
content: [
|
|
32050
31971
|
{
|
|
32051
31972
|
type: "text",
|
|
32052
|
-
text:
|
|
31973
|
+
text: defectReport(where, error, {
|
|
32053
31974
|
seenBefore,
|
|
32054
31975
|
godotVersion: this.lastGodotVersion ?? undefined
|
|
32055
31976
|
})
|
|
@@ -32391,11 +32312,16 @@ class GodotServer {
|
|
|
32391
32312
|
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
|
|
32392
32313
|
case "runtime_input":
|
|
32393
32314
|
if (op === "click") {
|
|
32315
|
+
if (readNonEmptyString(args, "nodePath") === undefined && readNonEmptyString(args, "says") === undefined) {
|
|
32316
|
+
return this.createErrorResponse("runtime_input click needs nodePath, or says with the words on the control to click.");
|
|
32317
|
+
}
|
|
32394
32318
|
return await this.handleRuntimeCommand("click", {
|
|
32395
32319
|
...whichGame(args),
|
|
32396
32320
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
32397
32321
|
button: readString(args, "button") ?? "left",
|
|
32398
|
-
double: readBoolean(args, "doubleClick") ?? false
|
|
32322
|
+
double: readBoolean(args, "doubleClick") ?? false,
|
|
32323
|
+
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
32324
|
+
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
32399
32325
|
});
|
|
32400
32326
|
}
|
|
32401
32327
|
if (op === "choose") {
|
|
@@ -33232,6 +33158,12 @@ class GodotServer {
|
|
|
33232
33158
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
33233
33159
|
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));
|
|
33234
33160
|
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
33161
|
+
const marker = readNonEmptyString(args, "printed");
|
|
33162
|
+
const echoed = marker === undefined ? undefined : run.log.select({ severity: "info", sinceLastCall: false, contains: marker, limit: 200 });
|
|
33163
|
+
const printedLines = echoed === undefined ? {} : {
|
|
33164
|
+
printed: echoed.entries.map((entry) => entry.text),
|
|
33165
|
+
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
33166
|
+
};
|
|
33235
33167
|
if (report === null) {
|
|
33236
33168
|
const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).`;
|
|
33237
33169
|
return {
|
|
@@ -33250,6 +33182,7 @@ class GodotServer {
|
|
|
33250
33182
|
...scriptErrors.length > 0 ? { scriptErrors } : {},
|
|
33251
33183
|
arguments: cmdArgs,
|
|
33252
33184
|
entries: forAnswer(printed.slice(0, 60).map(aboveTheRunner)),
|
|
33185
|
+
...printedLines,
|
|
33253
33186
|
savesNote,
|
|
33254
33187
|
...scanned
|
|
33255
33188
|
}, null, 2)
|
|
@@ -33286,6 +33219,7 @@ class GodotServer {
|
|
|
33286
33219
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
33287
33220
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
33288
33221
|
notRun: notRun > 0 ? notRun : undefined,
|
|
33222
|
+
...printedLines,
|
|
33289
33223
|
savesNote,
|
|
33290
33224
|
...scanned,
|
|
33291
33225
|
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." : notRun > 0 ? `This run stopped at the first failure in each suite it failed in, so ${notRun} case${notRun === 1 ? "" : "s"} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : undefined,
|
|
@@ -34671,15 +34605,18 @@ class GodotServer {
|
|
|
34671
34605
|
"editor_output with no op answers about the game a run is playing, which is a different log"
|
|
34672
34606
|
]);
|
|
34673
34607
|
}
|
|
34674
|
-
const
|
|
34608
|
+
const asked = readString(args, "severity");
|
|
34609
|
+
const severity = asked === "error" || asked === "warning" ? asked : "info";
|
|
34610
|
+
const contains = readNonEmptyString(args, "contains");
|
|
34675
34611
|
const selected = console2.log.select({
|
|
34676
|
-
severity
|
|
34612
|
+
severity,
|
|
34677
34613
|
sinceLastCall: false,
|
|
34678
|
-
contains
|
|
34614
|
+
contains,
|
|
34679
34615
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
34680
34616
|
});
|
|
34681
34617
|
const everything = console2.log.everything();
|
|
34682
|
-
const
|
|
34618
|
+
const filtered = severity !== "info" || contains !== undefined;
|
|
34619
|
+
const grouped = bursts(everything, readNonNegativeNumber(args, "before") ?? 3, filtered ? (entry) => answersTo(entry, severity, contains) : undefined);
|
|
34683
34620
|
const shown = grouped.slice(0, MOST_GROUPS);
|
|
34684
34621
|
return this.jsonTextResponse({
|
|
34685
34622
|
editorPid: editorPid ?? undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
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",
|