gdharness 1.1.40 → 1.1.42
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 +54 -10
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +55 -24
- package/build/index.js +54 -10
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -17592,8 +17592,9 @@ var init_tool_definitions = __esm(() => {
|
|
|
17592
17592
|
parameters: {
|
|
17593
17593
|
projectPath: PROJECT_PATH,
|
|
17594
17594
|
path: {
|
|
17595
|
-
type: "string",
|
|
17596
|
-
|
|
17595
|
+
type: ["string", "array"],
|
|
17596
|
+
items: { type: "string" },
|
|
17597
|
+
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."
|
|
17597
17598
|
},
|
|
17598
17599
|
ignore: {
|
|
17599
17600
|
type: "array",
|
|
@@ -42085,6 +42086,36 @@ function elsewhereIn(projectPath, asked) {
|
|
|
42085
42086
|
const there = near.map((name) => `res://${name}`).join(" and ");
|
|
42086
42087
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
42087
42088
|
}
|
|
42089
|
+
function testPathsIn(projectPath, given) {
|
|
42090
|
+
const entries = given === undefined || given === null || given === "" ? ["test"] : given;
|
|
42091
|
+
const listed = Array.isArray(entries) ? entries : [entries];
|
|
42092
|
+
if (listed.length === 0) {
|
|
42093
|
+
return {
|
|
42094
|
+
ok: false,
|
|
42095
|
+
reason: "path is an empty list: name a suite file or a directory, or leave path out for test."
|
|
42096
|
+
};
|
|
42097
|
+
}
|
|
42098
|
+
const resolved = [];
|
|
42099
|
+
for (const [index, entry] of listed.entries()) {
|
|
42100
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
42101
|
+
return {
|
|
42102
|
+
ok: false,
|
|
42103
|
+
reason: `path${Array.isArray(entries) ? `[${index}]` : ""} is ${typeof entry === "string" ? "empty" : describeValue(entry)}, not a suite file or a directory in the project.`
|
|
42104
|
+
};
|
|
42105
|
+
}
|
|
42106
|
+
const location = resolveWithinProject(projectPath, entry);
|
|
42107
|
+
if (!location.ok) {
|
|
42108
|
+
return { ok: false, reason: location.reason };
|
|
42109
|
+
}
|
|
42110
|
+
resolved.push(location.relativePath.replace(/\/+$/, ""));
|
|
42111
|
+
}
|
|
42112
|
+
const unique = [...new Set(resolved)];
|
|
42113
|
+
const within = (path, directory) => path.startsWith(`${directory}/`);
|
|
42114
|
+
return {
|
|
42115
|
+
ok: true,
|
|
42116
|
+
value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
|
|
42117
|
+
};
|
|
42118
|
+
}
|
|
42088
42119
|
function hasMainScene(projectFile) {
|
|
42089
42120
|
const scene = parseProjectGodot(readFileSync21(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
42090
42121
|
return typeof scene === "string" && scene !== "";
|
|
@@ -43600,9 +43631,24 @@ class GodotServer {
|
|
|
43600
43631
|
if (!project.ok) {
|
|
43601
43632
|
return project.response;
|
|
43602
43633
|
}
|
|
43603
|
-
const
|
|
43604
|
-
if (!
|
|
43605
|
-
return
|
|
43634
|
+
const paths = testPathsIn(project.value.path, args["path"]);
|
|
43635
|
+
if (!paths.ok) {
|
|
43636
|
+
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
43637
|
+
}
|
|
43638
|
+
const asked = paths.value;
|
|
43639
|
+
const missing = asked.filter((path) => !existsSync19(join24(project.value.path, path.slice("res://".length))));
|
|
43640
|
+
if (missing.length > 0) {
|
|
43641
|
+
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
43642
|
+
return {
|
|
43643
|
+
content: [
|
|
43644
|
+
{
|
|
43645
|
+
type: "text",
|
|
43646
|
+
text: `No tests ran: ${verdict}.${elsewhereIn(project.value.path, missing[0] ?? "")}`
|
|
43647
|
+
},
|
|
43648
|
+
{ type: "text", text: answerJson({ passed: false, verdict, tests: 0 }) }
|
|
43649
|
+
],
|
|
43650
|
+
isError: true
|
|
43651
|
+
};
|
|
43606
43652
|
}
|
|
43607
43653
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
43608
43654
|
if (!existsSync19(join24(project.value.path, runner))) {
|
|
@@ -43622,7 +43668,6 @@ class GodotServer {
|
|
|
43622
43668
|
const ours = `gdharness-reports/${randomUUID2()}`;
|
|
43623
43669
|
const reports = `res://.godot/${ours}`;
|
|
43624
43670
|
const ignored = readStringArray(args, "ignore") ?? [];
|
|
43625
|
-
const asked = readString(contained.value, "path") ?? "res://test";
|
|
43626
43671
|
const cmdArgs = [
|
|
43627
43672
|
"--headless",
|
|
43628
43673
|
"--path",
|
|
@@ -43631,8 +43676,7 @@ class GodotServer {
|
|
|
43631
43676
|
`res://${runner}`,
|
|
43632
43677
|
"--ignoreHeadlessMode",
|
|
43633
43678
|
...readBoolean(args, "failFast") === true ? [] : ["-c"],
|
|
43634
|
-
"-a",
|
|
43635
|
-
asked,
|
|
43679
|
+
...asked.flatMap((path) => ["-a", path]),
|
|
43636
43680
|
...ignored.flatMap((entry) => ["-i", entry]),
|
|
43637
43681
|
"-rd",
|
|
43638
43682
|
reports,
|
|
@@ -43705,7 +43749,7 @@ class GodotServer {
|
|
|
43705
43749
|
limit: Number.POSITIVE_INFINITY
|
|
43706
43750
|
}).entries;
|
|
43707
43751
|
const said = printed.map((entry) => entry.text);
|
|
43708
|
-
const nothingRan = timedOut ? null : whyNoReport(said, asked);
|
|
43752
|
+
const nothingRan = timedOut ? null : whyNoReport(said, asked.join(" and "));
|
|
43709
43753
|
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
43710
43754
|
const hung = cutShort?.hung ?? false;
|
|
43711
43755
|
const scriptErrors = timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
|
|
@@ -43719,7 +43763,7 @@ class GodotServer {
|
|
|
43719
43763
|
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
43720
43764
|
};
|
|
43721
43765
|
if (report === null) {
|
|
43722
|
-
const note = nothingRan !== null ? `No tests ran: ${verdict}
|
|
43766
|
+
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}).`;
|
|
43723
43767
|
return {
|
|
43724
43768
|
content: [
|
|
43725
43769
|
{ type: "text", text: note },
|
|
@@ -288,6 +288,10 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
|
|
|
288
288
|
## the click scrolls it into view first: what is drawn over its place now is not what it will land
|
|
289
289
|
## under. Judged not covered at all instead, a button below the fold of a roster that a full-screen
|
|
290
290
|
## page covered was picked over the ones in view, scrolled up under the page and pressed there.
|
|
291
|
+
## Whatever else the scroll holds moves with it and is judged where it goes. Judged where it sat, the
|
|
292
|
+
## card at the top of a tray scrolled down was drawn over a button above the view, because a later
|
|
293
|
+
## card is drawn over an earlier one, and a button below the view was passed by the same card only
|
|
294
|
+
## because the cards beside the bottom edge come before it.
|
|
291
295
|
##
|
|
292
296
|
## [param areas] holds the box around each of [param drawn], so everything whose box misses the
|
|
293
297
|
## point is passed over before anything costlier is asked of it. A window's box is in the space of
|
|
@@ -295,7 +299,8 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
|
|
|
295
299
|
## so a window is judged by [method _window_over] instead.
|
|
296
300
|
static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2]) -> Node:
|
|
297
301
|
var viewport: Viewport = target.get_viewport()
|
|
298
|
-
var
|
|
302
|
+
var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
|
|
303
|
+
var point: Vector2 = _centre_of(target) + _moved_by(target, moves)
|
|
299
304
|
var layer: int = _layer_of(target)
|
|
300
305
|
for at: int in drawn.size():
|
|
301
306
|
var other: Node = drawn[at]
|
|
@@ -304,9 +309,11 @@ static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2])
|
|
|
304
309
|
if _window_over(target, point, window):
|
|
305
310
|
return window
|
|
306
311
|
continue
|
|
307
|
-
if not areas[at].has_point(point):
|
|
308
|
-
continue
|
|
309
312
|
var control: Control = other
|
|
313
|
+
# The point where this control is now, which is where the scroll will carry it to the point.
|
|
314
|
+
var before: Vector2 = point - _moved_by(control, moves)
|
|
315
|
+
if not areas[at].has_point(before):
|
|
316
|
+
continue
|
|
310
317
|
if control == target or control.is_ancestor_of(target) or target.is_ancestor_of(control):
|
|
311
318
|
continue
|
|
312
319
|
if control.get_viewport() != viewport:
|
|
@@ -314,18 +321,34 @@ static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2])
|
|
|
314
321
|
var its_layer: int = _layer_of(control)
|
|
315
322
|
if its_layer < layer or (its_layer == layer and not control.is_greater_than(target)):
|
|
316
323
|
continue
|
|
317
|
-
if _holds(control,
|
|
324
|
+
if _holds(control, before) and not _clipped_away(control, point, moves):
|
|
318
325
|
return control
|
|
319
326
|
return null
|
|
320
327
|
|
|
321
328
|
|
|
322
|
-
##
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
+
## [param target]'s centre in its viewport, where it is now.
|
|
330
|
+
static func _centre_of(target: Control) -> Vector2:
|
|
331
|
+
return target.get_global_transform_with_canvas() * (target.size * 0.5)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
## How far the click moves what each ScrollContainer holding [param target] holds, to bring it into
|
|
335
|
+
## view: empty when no container clips it away, as nothing is scrolled then.
|
|
336
|
+
static func _scrolls_for(target: Control) -> Dictionary[ScrollContainer, Vector2]:
|
|
337
|
+
var none: Dictionary[ScrollContainer, Vector2] = {}
|
|
338
|
+
var centre: Vector2 = _centre_of(target)
|
|
339
|
+
if not _clipped_away(target, centre, none):
|
|
340
|
+
return none
|
|
341
|
+
return _where_it_lands(target, centre)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
## How far the scrolls in [param moves] carry [param node]: the sum over the containers holding it,
|
|
345
|
+
## and nothing for a container itself, which stays where it is while what it holds moves.
|
|
346
|
+
static func _moved_by(node: Node, moves: Dictionary[ScrollContainer, Vector2]) -> Vector2:
|
|
347
|
+
var moved: Vector2 = Vector2.ZERO
|
|
348
|
+
for holder: ScrollContainer in moves:
|
|
349
|
+
if holder.is_ancestor_of(node):
|
|
350
|
+
moved += moves[holder]
|
|
351
|
+
return moved
|
|
329
352
|
|
|
330
353
|
|
|
331
354
|
## Whether the embedded [param window] is drawn over [param point], [param target]'s centre in its
|
|
@@ -366,14 +389,15 @@ static func _holds(control: Control, point: Vector2) -> bool:
|
|
|
366
389
|
return Rect2(Vector2.ZERO, control.size).has_point(placed.affine_inverse() * point)
|
|
367
390
|
|
|
368
391
|
|
|
369
|
-
##
|
|
370
|
-
## view:
|
|
371
|
-
## first, which is how one brings a control into view. A target larger than a
|
|
372
|
-
## in it. Only a ScrollContainer, because that is all a click scrolls: another
|
|
373
|
-
## what it holds, a collapsed section for one, leaves the target where it is.
|
|
374
|
-
static func _where_it_lands(target: Control, point: Vector2) -> Vector2:
|
|
392
|
+
## How far each ScrollContainer holding [param target] moves what it holds once the click has
|
|
393
|
+
## scrolled [param target], centred at [param point], into view: as little as puts the whole of it
|
|
394
|
+
## inside each, innermost first, which is how one brings a control into view. A target larger than a
|
|
395
|
+
## container is centred in it. Only a ScrollContainer, because that is all a click scrolls: another
|
|
396
|
+
## container clipping what it holds, a collapsed section for one, leaves the target where it is.
|
|
397
|
+
static func _where_it_lands(target: Control, point: Vector2) -> Dictionary[ScrollContainer, Vector2]:
|
|
375
398
|
var drawn: Transform2D = target.get_global_transform_with_canvas()
|
|
376
399
|
var half: Vector2 = (drawn * Rect2(Vector2.ZERO, target.size)).size * 0.5
|
|
400
|
+
var moves: Dictionary[ScrollContainer, Vector2] = {}
|
|
377
401
|
var landed: Vector2 = point
|
|
378
402
|
var walk: Node = target.get_parent()
|
|
379
403
|
while walk != null and not (walk is Viewport):
|
|
@@ -382,12 +406,15 @@ static func _where_it_lands(target: Control, point: Vector2) -> Vector2:
|
|
|
382
406
|
var shown: Rect2 = holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
|
|
383
407
|
var low: Vector2 = shown.position + half
|
|
384
408
|
var high: Vector2 = shown.end - half
|
|
385
|
-
|
|
409
|
+
var next: Vector2 = Vector2(
|
|
386
410
|
shown.get_center().x if low.x > high.x else clampf(landed.x, low.x, high.x),
|
|
387
411
|
shown.get_center().y if low.y > high.y else clampf(landed.y, low.y, high.y)
|
|
388
412
|
)
|
|
413
|
+
if next != landed:
|
|
414
|
+
moves[holder] = next - landed
|
|
415
|
+
landed = next
|
|
389
416
|
walk = walk.get_parent()
|
|
390
|
-
return
|
|
417
|
+
return moves
|
|
391
418
|
|
|
392
419
|
|
|
393
420
|
## Whether a click could not reach [param target] however it scrolled: its centre, where the click
|
|
@@ -395,8 +422,9 @@ static func _where_it_lands(target: Control, point: Vector2) -> Vector2:
|
|
|
395
422
|
## container the click does not scroll. A drawer parked off the side of the screen, or anything
|
|
396
423
|
## outside the 64 by 64 viewport of a game with no window, was listed among the controls on screen.
|
|
397
424
|
static func _off_screen(target: Control) -> bool:
|
|
398
|
-
var
|
|
399
|
-
|
|
425
|
+
var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
|
|
426
|
+
var point: Vector2 = _centre_of(target) + _moved_by(target, moves)
|
|
427
|
+
if _clipped_away(target, point, moves) or not target.get_viewport().get_visible_rect().has_point(point):
|
|
400
428
|
return true
|
|
401
429
|
var reached: Dictionary = Screen.reach(target.get_viewport(), point)
|
|
402
430
|
var viewport: Viewport = reached["viewport"]
|
|
@@ -404,16 +432,19 @@ static func _off_screen(target: Control) -> bool:
|
|
|
404
432
|
return not viewport.get_visible_rect().has_point(there)
|
|
405
433
|
|
|
406
434
|
|
|
407
|
-
## Whether a container clipping what it holds cuts [param point] off from [param control]
|
|
435
|
+
## Whether a container clipping what it holds cuts [param point] off from [param control], once the
|
|
436
|
+
## scrolls in [param moves] have carried each container wherever they carry it.
|
|
408
437
|
##
|
|
409
438
|
## Only up to the viewport [param control] is in. A dialog is a window, and the containers the game
|
|
410
439
|
## added it under are in another space: judged against a clipping one of those, every button of a
|
|
411
440
|
## confirmation was refused as off the screen while it sat in plain view.
|
|
412
|
-
static func _clipped_away(
|
|
441
|
+
static func _clipped_away(
|
|
442
|
+
control: Control, point: Vector2, moves: Dictionary[ScrollContainer, Vector2]
|
|
443
|
+
) -> bool:
|
|
413
444
|
var walk: Node = control.get_parent()
|
|
414
445
|
while walk != null and not (walk is Viewport):
|
|
415
446
|
var holder: Control = walk as Control
|
|
416
|
-
if holder != null and holder.clip_contents and not _holds(holder, point):
|
|
447
|
+
if holder != null and holder.clip_contents and not _holds(holder, point - _moved_by(holder, moves)):
|
|
417
448
|
return true
|
|
418
449
|
walk = walk.get_parent()
|
|
419
450
|
return false
|
package/build/index.js
CHANGED
|
@@ -33013,8 +33013,9 @@ var TOOL_SPECS = [
|
|
|
33013
33013
|
parameters: {
|
|
33014
33014
|
projectPath: PROJECT_PATH,
|
|
33015
33015
|
path: {
|
|
33016
|
-
type: "string",
|
|
33017
|
-
|
|
33016
|
+
type: ["string", "array"],
|
|
33017
|
+
items: { type: "string" },
|
|
33018
|
+
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."
|
|
33018
33019
|
},
|
|
33019
33020
|
ignore: {
|
|
33020
33021
|
type: "array",
|
|
@@ -34659,6 +34660,36 @@ function elsewhereIn(projectPath, asked) {
|
|
|
34659
34660
|
const there = near.map((name) => `res://${name}`).join(" and ");
|
|
34660
34661
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
34661
34662
|
}
|
|
34663
|
+
function testPathsIn(projectPath, given) {
|
|
34664
|
+
const entries = given === undefined || given === null || given === "" ? ["test"] : given;
|
|
34665
|
+
const listed = Array.isArray(entries) ? entries : [entries];
|
|
34666
|
+
if (listed.length === 0) {
|
|
34667
|
+
return {
|
|
34668
|
+
ok: false,
|
|
34669
|
+
reason: "path is an empty list: name a suite file or a directory, or leave path out for test."
|
|
34670
|
+
};
|
|
34671
|
+
}
|
|
34672
|
+
const resolved = [];
|
|
34673
|
+
for (const [index, entry] of listed.entries()) {
|
|
34674
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
34675
|
+
return {
|
|
34676
|
+
ok: false,
|
|
34677
|
+
reason: `path${Array.isArray(entries) ? `[${index}]` : ""} is ${typeof entry === "string" ? "empty" : describeValue(entry)}, not a suite file or a directory in the project.`
|
|
34678
|
+
};
|
|
34679
|
+
}
|
|
34680
|
+
const location = resolveWithinProject(projectPath, entry);
|
|
34681
|
+
if (!location.ok) {
|
|
34682
|
+
return { ok: false, reason: location.reason };
|
|
34683
|
+
}
|
|
34684
|
+
resolved.push(location.relativePath.replace(/\/+$/, ""));
|
|
34685
|
+
}
|
|
34686
|
+
const unique = [...new Set(resolved)];
|
|
34687
|
+
const within = (path, directory) => path.startsWith(`${directory}/`);
|
|
34688
|
+
return {
|
|
34689
|
+
ok: true,
|
|
34690
|
+
value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
|
|
34691
|
+
};
|
|
34692
|
+
}
|
|
34662
34693
|
function hasMainScene(projectFile) {
|
|
34663
34694
|
const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
34664
34695
|
return typeof scene === "string" && scene !== "";
|
|
@@ -36174,9 +36205,24 @@ class GodotServer {
|
|
|
36174
36205
|
if (!project.ok) {
|
|
36175
36206
|
return project.response;
|
|
36176
36207
|
}
|
|
36177
|
-
const
|
|
36178
|
-
if (!
|
|
36179
|
-
return
|
|
36208
|
+
const paths = testPathsIn(project.value.path, args["path"]);
|
|
36209
|
+
if (!paths.ok) {
|
|
36210
|
+
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
36211
|
+
}
|
|
36212
|
+
const asked = paths.value;
|
|
36213
|
+
const missing = asked.filter((path) => !existsSync16(join20(project.value.path, path.slice("res://".length))));
|
|
36214
|
+
if (missing.length > 0) {
|
|
36215
|
+
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
36216
|
+
return {
|
|
36217
|
+
content: [
|
|
36218
|
+
{
|
|
36219
|
+
type: "text",
|
|
36220
|
+
text: `No tests ran: ${verdict}.${elsewhereIn(project.value.path, missing[0] ?? "")}`
|
|
36221
|
+
},
|
|
36222
|
+
{ type: "text", text: answerJson({ passed: false, verdict, tests: 0 }) }
|
|
36223
|
+
],
|
|
36224
|
+
isError: true
|
|
36225
|
+
};
|
|
36180
36226
|
}
|
|
36181
36227
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
36182
36228
|
if (!existsSync16(join20(project.value.path, runner))) {
|
|
@@ -36196,7 +36242,6 @@ class GodotServer {
|
|
|
36196
36242
|
const ours = `gdharness-reports/${randomUUID2()}`;
|
|
36197
36243
|
const reports = `res://.godot/${ours}`;
|
|
36198
36244
|
const ignored = readStringArray(args, "ignore") ?? [];
|
|
36199
|
-
const asked = readString(contained.value, "path") ?? "res://test";
|
|
36200
36245
|
const cmdArgs = [
|
|
36201
36246
|
"--headless",
|
|
36202
36247
|
"--path",
|
|
@@ -36205,8 +36250,7 @@ class GodotServer {
|
|
|
36205
36250
|
`res://${runner}`,
|
|
36206
36251
|
"--ignoreHeadlessMode",
|
|
36207
36252
|
...readBoolean(args, "failFast") === true ? [] : ["-c"],
|
|
36208
|
-
"-a",
|
|
36209
|
-
asked,
|
|
36253
|
+
...asked.flatMap((path) => ["-a", path]),
|
|
36210
36254
|
...ignored.flatMap((entry) => ["-i", entry]),
|
|
36211
36255
|
"-rd",
|
|
36212
36256
|
reports,
|
|
@@ -36279,7 +36323,7 @@ class GodotServer {
|
|
|
36279
36323
|
limit: Number.POSITIVE_INFINITY
|
|
36280
36324
|
}).entries;
|
|
36281
36325
|
const said = printed.map((entry) => entry.text);
|
|
36282
|
-
const nothingRan = timedOut ? null : whyNoReport(said, asked);
|
|
36326
|
+
const nothingRan = timedOut ? null : whyNoReport(said, asked.join(" and "));
|
|
36283
36327
|
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
36284
36328
|
const hung = cutShort?.hung ?? false;
|
|
36285
36329
|
const scriptErrors = timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
|
|
@@ -36293,7 +36337,7 @@ class GodotServer {
|
|
|
36293
36337
|
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
36294
36338
|
};
|
|
36295
36339
|
if (report === null) {
|
|
36296
|
-
const note = nothingRan !== null ? `No tests ran: ${verdict}
|
|
36340
|
+
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}).`;
|
|
36297
36341
|
return {
|
|
36298
36342
|
content: [
|
|
36299
36343
|
{ type: "text", text: note },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.42",
|
|
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",
|