gdharness 1.2.1 → 1.2.3

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
@@ -17977,7 +17977,7 @@ var init_tool_definitions = __esm(() => {
17977
17977
  },
17978
17978
  {
17979
17979
  name: "project_test",
17980
- 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; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. 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. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. 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. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
17980
+ 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. Each entry under failed carries its first report as message and detail, and, when it has more than one, every report under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. 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; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. 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. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. 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. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
17981
17981
  parameters: {
17982
17982
  projectPath: PROJECT_PATH,
17983
17983
  path: {
@@ -39250,18 +39250,25 @@ function count(attributes, name) {
39250
39250
  return Number.isFinite(value) ? value : 0;
39251
39251
  }
39252
39252
  function caseOf(suite, element) {
39253
- const outcome = element.children.find((child) => ["failure", "error", "skipped"].includes(child.name));
39254
- const status = outcome === undefined ? "passed" : outcome.name === "failure" ? "failed" : outcome.name === "error" ? "error" : "skipped";
39255
- const detail = outcome?.text.trim() ?? "";
39253
+ const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
39254
+ const detail = child.text.trim();
39255
+ return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
39256
+ });
39257
+ const reports = [...of("failure"), ...of("error"), ...of("skipped")];
39258
+ const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
39256
39259
  return {
39257
39260
  suite: suite.attributes["name"] ?? "",
39258
39261
  name: element.attributes["name"] ?? "",
39259
39262
  status,
39260
39263
  time: count(element.attributes, "time"),
39261
- message: outcome?.attributes["message"] ?? null,
39262
- detail: detail === "" ? null : detail
39264
+ message: reports[0]?.message ?? null,
39265
+ detail: reports[0]?.detail ?? null,
39266
+ reports
39263
39267
  };
39264
39268
  }
39269
+ function failedAnAssertion(entry) {
39270
+ return entry.reports.some((report) => report.kind === "failure");
39271
+ }
39265
39272
  function pathOf(suite) {
39266
39273
  const directory = suite.attributes["package"];
39267
39274
  const name = suite.attributes["name"];
@@ -39276,7 +39283,7 @@ function parseJUnit(xml) {
39276
39283
  const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
39277
39284
  const suites = suiteElements.map((suite) => {
39278
39285
  const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
39279
- const failures = cases.filter((entry) => entry.status === "failed").length;
39286
+ const failures = cases.filter(failedAnAssertion).length;
39280
39287
  const errors = cases.filter((entry) => entry.status === "error").length;
39281
39288
  const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
39282
39289
  return {
@@ -39296,7 +39303,7 @@ function parseJUnit(xml) {
39296
39303
  const all = suites.flatMap((suite) => suite.cases);
39297
39304
  return {
39298
39305
  tests: all.length,
39299
- failures: all.filter((entry) => entry.status === "failed").length,
39306
+ failures: all.filter(failedAnAssertion).length,
39300
39307
  errors: all.filter((entry) => entry.status === "error").length,
39301
39308
  hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
39302
39309
  hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
@@ -39418,6 +39425,25 @@ ${note}`;
39418
39425
  return detail === entry.detail ? entry : { ...entry, detail };
39419
39426
  });
39420
39427
  }
39428
+ function failedCases(report, printed) {
39429
+ const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
39430
+ const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
39431
+ let next = 0;
39432
+ return cases.map(({ entry, path }) => {
39433
+ const reports = read.slice(next, next + entry.reports.length);
39434
+ next += entry.reports.length;
39435
+ return {
39436
+ suite: entry.suite,
39437
+ name: entry.name,
39438
+ status: entry.status,
39439
+ time: entry.time,
39440
+ path,
39441
+ message: reports[0]?.message ?? null,
39442
+ detail: reports[0]?.detail ?? null,
39443
+ ...reports.length > 1 ? { reports } : {}
39444
+ };
39445
+ });
39446
+ }
39421
39447
  function masked(expected, actual) {
39422
39448
  const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
39423
39449
  return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
@@ -42840,6 +42866,9 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
42840
42866
  function noCodeWillCome(run) {
42841
42867
  return !exited(run);
42842
42868
  }
42869
+ function transcriptNote(transcript, going) {
42870
+ return going ? `Everything this run has printed is in ${transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.` : `Everything this run printed is in ${transcript}, uncapped and complete, since the run is over.`;
42871
+ }
42843
42872
  function stopWaitMs() {
42844
42873
  return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
42845
42874
  }
@@ -45432,7 +45461,7 @@ class GodotServer {
45432
45461
  isError: true
45433
45462
  };
45434
45463
  }
45435
- const failed = withActualsPrinted(report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path }))), Buffer.concat(console2).toString("utf8"));
45464
+ const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
45436
45465
  const orphans = orphansPrinted(said);
45437
45466
  const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
45438
45467
  const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
@@ -47575,7 +47604,7 @@ class GodotServer {
47575
47604
  notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
47576
47605
  }
47577
47606
  if (run.transcript !== null && selected.omitted > 0) {
47578
- notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
47607
+ notes.push(transcriptNote(run.transcript, going));
47579
47608
  }
47580
47609
  return this.jsonTextResponse({
47581
47610
  running: going,
@@ -377,10 +377,15 @@ static func drawn_over(target: Control) -> Node:
377
377
 
378
378
 
379
379
  ## Why [param node], which is not visible in the tree, is not, as a parenthesis to follow its path:
380
- ## which nodes hide it, nearest first, and what is shown beside the nearest. Reading visible off
381
- ## the node itself says true when only an ancestor is hidden, so without this a caller had nothing
382
- ## to unhide; every hidden one is named because unhiding only the nearest leaves the rest hiding it.
383
- ## "" when nothing on the way up says so.
380
+ ## which nodes hide it, nearest first, and what has taken its place. Reading visible off the node
381
+ ## itself says true when only an ancestor is hidden, so without this a caller had nothing to unhide;
382
+ ## every hidden one is named because unhiding only the nearest leaves the rest hiding it.
383
+ ##
384
+ ## What has taken its place is what a click at its centre reaches now, which is the screen a game
385
+ ## shows instead. Without a control there, as for a 3D node, it is what is shown beside the
386
+ ## outermost hidden node: a game hides a screen by hiding the screen's root, and the nearest hidden
387
+ ## node can be a panel deep inside it whose siblings are hidden with it. "" when nothing on the way
388
+ ## up says the node is hidden.
384
389
  static func why_hidden(node: Node) -> String:
385
390
  var hiding: Array[Node] = _hiding(node)
386
391
  if hiding.is_empty():
@@ -391,27 +396,71 @@ static func why_hidden(node: Node) -> String:
391
396
  named.append("its own visible is false")
392
397
  else:
393
398
  named.append("%s is hidden%s" % [one.get_path(), "" if named.is_empty() else " too"])
399
+ var control: Control = node as Control
400
+ var instead: Node = null if control == null else drawn_at(control)
401
+ if instead != null:
402
+ return " (%s; %s is drawn where it would be)" % [", and ".join(named), instead.get_path()]
403
+ var outermost: Node = hiding.back()
394
404
  var beside: Array[String] = []
395
- var parent: Node = hiding[0].get_parent()
405
+ var parent: Node = outermost.get_parent()
396
406
  if parent != null:
397
407
  for sibling: Node in parent.get_children():
398
- if sibling != hiding[0] and _shown(sibling):
408
+ if sibling != outermost and _shown(sibling):
399
409
  beside.append(str(sibling.get_path()))
400
- return " (%s%s)" % [", and ".join(named), _shown_beside(beside)]
410
+ return " (%s%s)" % [", and ".join(named), _shown_beside(beside, outermost)]
411
+
412
+
413
+ ## What a click at the centre of [param target], which is hidden, would reach instead, or null.
414
+ ##
415
+ ## The topmost thing there that takes the pointer, whatever order it is drawn in relative to the
416
+ ## target, since the target is not drawn at all; and never a node holding the target, which covers
417
+ ## its place by containing it and is not what took it.
418
+ static func drawn_at(target: Control) -> Node:
419
+ var drawn: Array[Node] = _pointer_takers(target.get_tree().root)
420
+ var point: Vector2 = _centre_of(target)
421
+ var none: Dictionary[ScrollContainer, Vector2] = {}
422
+ var viewport: Viewport = target.get_viewport()
423
+ var best: Control = null
424
+ for each: Node in drawn:
425
+ var window: Window = each as Window
426
+ if window != null:
427
+ if _window_over(target, point, window):
428
+ return window
429
+ continue
430
+ var control: Control = each
431
+ if control.is_ancestor_of(target) or target.is_ancestor_of(control):
432
+ continue
433
+ if control.get_viewport() != viewport or not _holds(control, point):
434
+ continue
435
+ if _clipped_away(control, point, none):
436
+ continue
437
+ if best == null or _drawn_above(control, best):
438
+ best = control
439
+ return best
440
+
441
+
442
+ ## Whether [param control] is drawn above [param other]: on a higher canvas layer, or later on the
443
+ ## same one.
444
+ static func _drawn_above(control: Control, other: Control) -> bool:
445
+ var layer: int = _layer_of(control)
446
+ var other_layer: int = _layer_of(other)
447
+ return layer > other_layer or (layer == other_layer and control.is_greater_than(other))
401
448
 
402
449
 
403
- ## "; A, B and C beside it are shown" for [param beside], naming three at most and counting the rest.
404
- static func _shown_beside(beside: Array[String]) -> String:
450
+ ## "; A, B and C are shown beside N" for [param beside] and the hidden node [param hidden], naming
451
+ ## three at most and counting the rest.
452
+ static func _shown_beside(beside: Array[String], hidden: Node) -> String:
405
453
  if beside.is_empty():
406
454
  return ""
455
+ var at: String = str(hidden.get_path())
407
456
  if beside.size() == 1:
408
- return "; %s beside it is shown" % beside[0]
457
+ return "; %s is shown beside %s" % [beside[0], at]
409
458
  var listed: Array[String] = beside.slice(0, 3)
410
459
  var rest: int = beside.size() - listed.size()
411
460
  if rest > 0:
412
- return "; %s and %d more beside it are shown" % [", ".join(listed), rest]
461
+ return "; %s and %d more are shown beside %s" % [", ".join(listed), rest, at]
413
462
  var last: String = listed.pop_back()
414
- return "; %s and %s beside it are shown" % [", ".join(listed), last]
463
+ return "; %s and %s are shown beside %s" % [", ".join(listed), last, at]
415
464
 
416
465
 
417
466
  ## The nodes whose visible is false and keeps [param node] from being drawn, nearest first. Walked
package/build/index.js CHANGED
@@ -29817,18 +29817,25 @@ function count(attributes, name) {
29817
29817
  return Number.isFinite(value) ? value : 0;
29818
29818
  }
29819
29819
  function caseOf(suite, element) {
29820
- const outcome = element.children.find((child) => ["failure", "error", "skipped"].includes(child.name));
29821
- const status = outcome === undefined ? "passed" : outcome.name === "failure" ? "failed" : outcome.name === "error" ? "error" : "skipped";
29822
- const detail = outcome?.text.trim() ?? "";
29820
+ const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
29821
+ const detail = child.text.trim();
29822
+ return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
29823
+ });
29824
+ const reports = [...of("failure"), ...of("error"), ...of("skipped")];
29825
+ const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
29823
29826
  return {
29824
29827
  suite: suite.attributes["name"] ?? "",
29825
29828
  name: element.attributes["name"] ?? "",
29826
29829
  status,
29827
29830
  time: count(element.attributes, "time"),
29828
- message: outcome?.attributes["message"] ?? null,
29829
- detail: detail === "" ? null : detail
29831
+ message: reports[0]?.message ?? null,
29832
+ detail: reports[0]?.detail ?? null,
29833
+ reports
29830
29834
  };
29831
29835
  }
29836
+ function failedAnAssertion(entry) {
29837
+ return entry.reports.some((report) => report.kind === "failure");
29838
+ }
29832
29839
  function pathOf(suite) {
29833
29840
  const directory = suite.attributes["package"];
29834
29841
  const name = suite.attributes["name"];
@@ -29843,7 +29850,7 @@ function parseJUnit(xml) {
29843
29850
  const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
29844
29851
  const suites = suiteElements.map((suite) => {
29845
29852
  const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
29846
- const failures = cases.filter((entry) => entry.status === "failed").length;
29853
+ const failures = cases.filter(failedAnAssertion).length;
29847
29854
  const errors = cases.filter((entry) => entry.status === "error").length;
29848
29855
  const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
29849
29856
  return {
@@ -29863,7 +29870,7 @@ function parseJUnit(xml) {
29863
29870
  const all = suites.flatMap((suite) => suite.cases);
29864
29871
  return {
29865
29872
  tests: all.length,
29866
- failures: all.filter((entry) => entry.status === "failed").length,
29873
+ failures: all.filter(failedAnAssertion).length,
29867
29874
  errors: all.filter((entry) => entry.status === "error").length,
29868
29875
  hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
29869
29876
  hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
@@ -30010,6 +30017,25 @@ ${note}`;
30010
30017
  return detail === entry.detail ? entry : { ...entry, detail };
30011
30018
  });
30012
30019
  }
30020
+ function failedCases(report, printed) {
30021
+ const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
30022
+ const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
30023
+ let next = 0;
30024
+ return cases.map(({ entry, path }) => {
30025
+ const reports = read.slice(next, next + entry.reports.length);
30026
+ next += entry.reports.length;
30027
+ return {
30028
+ suite: entry.suite,
30029
+ name: entry.name,
30030
+ status: entry.status,
30031
+ time: entry.time,
30032
+ path,
30033
+ message: reports[0]?.message ?? null,
30034
+ detail: reports[0]?.detail ?? null,
30035
+ ...reports.length > 1 ? { reports } : {}
30036
+ };
30037
+ });
30038
+ }
30013
30039
  var MASKED_BRACKET = "[lb]";
30014
30040
  function masked(expected, actual) {
30015
30041
  const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
@@ -34054,7 +34080,7 @@ var TOOL_SPECS = [
34054
34080
  },
34055
34081
  {
34056
34082
  name: "project_test",
34057
- 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; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. 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. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. 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. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
34083
+ 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. Each entry under failed carries its first report as message and detail, and, when it has more than one, every report under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. 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; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. 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. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. 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. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
34058
34084
  parameters: {
34059
34085
  projectPath: PROJECT_PATH,
34060
34086
  path: {
@@ -35268,6 +35294,9 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
35268
35294
  function noCodeWillCome(run) {
35269
35295
  return !exited(run);
35270
35296
  }
35297
+ function transcriptNote(transcript, going) {
35298
+ return going ? `Everything this run has printed is in ${transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.` : `Everything this run printed is in ${transcript}, uncapped and complete, since the run is over.`;
35299
+ }
35271
35300
  function stopWaitMs() {
35272
35301
  return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
35273
35302
  }
@@ -37929,7 +37958,7 @@ class GodotServer {
37929
37958
  isError: true
37930
37959
  };
37931
37960
  }
37932
- const failed = withActualsPrinted(report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path }))), Buffer.concat(console2).toString("utf8"));
37961
+ const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
37933
37962
  const orphans = orphansPrinted(said);
37934
37963
  const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
37935
37964
  const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
@@ -40072,7 +40101,7 @@ class GodotServer {
40072
40101
  notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
40073
40102
  }
40074
40103
  if (run.transcript !== null && selected.omitted > 0) {
40075
- notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
40104
+ notes.push(transcriptNote(run.transcript, going));
40076
40105
  }
40077
40106
  return this.jsonTextResponse({
40078
40107
  running: going,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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",