gdharness 1.2.1 → 1.2.2

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);
@@ -45432,7 +45458,7 @@ class GodotServer {
45432
45458
  isError: true
45433
45459
  };
45434
45460
  }
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"));
45461
+ const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
45436
45462
  const orphans = orphansPrinted(said);
45437
45463
  const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
45438
45464
  const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
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: {
@@ -37929,7 +37955,7 @@ class GodotServer {
37929
37955
  isError: true
37930
37956
  };
37931
37957
  }
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"));
37958
+ const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
37933
37959
  const orphans = orphansPrinted(said);
37934
37960
  const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
37935
37961
  const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
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",