gdharness 1.1.54 → 1.1.56

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.
Files changed (3) hide show
  1. package/build/cli.js +152 -34
  2. package/build/index.js +152 -32
  3. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -7840,10 +7840,54 @@ function announced(line) {
7840
7840
  if (!match) {
7841
7841
  return null;
7842
7842
  }
7843
- return { severity: match[2] === "WARNING" ? "warning" : "error", text: match[3] ?? "" };
7843
+ return {
7844
+ severity: match[2] === "WARNING" ? "warning" : "error",
7845
+ text: match[3] ?? "",
7846
+ byScript: match[2] === "SCRIPT ERROR"
7847
+ };
7848
+ }
7849
+ function reported(entry) {
7850
+ return entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : { ...entry, detail: framesCollapsed(entry.detail) };
7851
+ }
7852
+ function framesCollapsed(detail) {
7853
+ const out = [];
7854
+ let pending;
7855
+ for (const line of detail) {
7856
+ const match = FRAME.exec(line);
7857
+ const number = match?.[1];
7858
+ const frame = match?.[2];
7859
+ if (frame !== undefined && pending?.frame === frame && Number(number) === Number(pending.last) + 1) {
7860
+ pending.last = String(number);
7861
+ continue;
7862
+ }
7863
+ if (pending !== undefined) {
7864
+ out.push(writtenRun(pending));
7865
+ }
7866
+ pending = number === undefined || frame === undefined ? undefined : { first: number, last: number, frame };
7867
+ if (pending === undefined) {
7868
+ out.push(line);
7869
+ }
7870
+ }
7871
+ if (pending !== undefined) {
7872
+ out.push(writtenRun(pending));
7873
+ }
7874
+ return out;
7844
7875
  }
7845
- function forAnswer(entries) {
7846
- return entries.map((entry) => entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : entry);
7876
+ function writtenRun(run) {
7877
+ return run.first === run.last ? `[${run.first}] ${run.frame}` : `[${run.first}-${run.last}] ${run.frame}`;
7878
+ }
7879
+ function foldedRepeats(entries) {
7880
+ const first = new Map;
7881
+ for (const entry of entries) {
7882
+ const key = JSON.stringify([entry.severity, entry.source, entry.text, entry.detail]);
7883
+ const seen = first.get(key);
7884
+ if (seen === undefined) {
7885
+ first.set(key, { entry, times: 1 });
7886
+ } else {
7887
+ seen.times += 1;
7888
+ }
7889
+ }
7890
+ return [...first.values()].map(({ entry, times }) => times === 1 ? reported(entry) : { ...reported(entry), times });
7847
7891
  }
7848
7892
 
7849
7893
  class GameLog {
@@ -7854,8 +7898,13 @@ class GameLog {
7854
7898
  transcript: new StringDecoder("utf8")
7855
7899
  };
7856
7900
  partial = { stdout: "", stderr: "", transcript: "" };
7857
- lastHeadline = null;
7901
+ lastHeadline = {
7902
+ stdout: null,
7903
+ stderr: null,
7904
+ transcript: null
7905
+ };
7858
7906
  seen = { error: 0, warning: 0, info: 0 };
7907
+ byScript = new Set;
7859
7908
  append(source, chunk) {
7860
7909
  const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
7861
7910
  const lines = (this.partial[source] + decoded).split(`
@@ -7879,8 +7928,9 @@ class GameLog {
7879
7928
  if (line.trim() === "") {
7880
7929
  return;
7881
7930
  }
7882
- if (this.lastHeadline && /^\s/.test(line) && this.lastHeadline.index === this.entries.length - 1) {
7883
- this.lastHeadline.detail.push(line.trim());
7931
+ const owner = this.lastHeadline[source];
7932
+ if (owner !== null && /^\s/.test(line)) {
7933
+ owner.detail.push(line.trim());
7884
7934
  return;
7885
7935
  }
7886
7936
  const headline = announced(line);
@@ -7893,7 +7943,13 @@ class GameLog {
7893
7943
  detail
7894
7944
  };
7895
7945
  this.entries.push(entry);
7896
- this.lastHeadline = headline ? { index: entry.index, detail } : null;
7946
+ if (headline?.byScript === true) {
7947
+ this.byScript.add(entry.index);
7948
+ }
7949
+ this.lastHeadline[source] = headline ? { detail } : null;
7950
+ }
7951
+ raisedByScript(entry) {
7952
+ return this.byScript.has(entry.index);
7897
7953
  }
7898
7954
  record(severity, text) {
7899
7955
  this.entries.push({
@@ -7903,7 +7959,6 @@ class GameLog {
7903
7959
  text,
7904
7960
  detail: []
7905
7961
  });
7906
- this.lastHeadline = null;
7907
7962
  }
7908
7963
  get all() {
7909
7964
  return this.entries;
@@ -7927,6 +7982,11 @@ class GameLog {
7927
7982
  const omitted = Math.max(0, matching.length - options.limit);
7928
7983
  return { entries: matching.slice(omitted), omitted };
7929
7984
  }
7985
+ selectFolded(options) {
7986
+ const folded = foldedRepeats(this.select({ ...options, limit: Number.POSITIVE_INFINITY }).entries);
7987
+ const omitted = Math.max(0, folded.length - options.limit);
7988
+ return { entries: folded.slice(omitted), omitted };
7989
+ }
7930
7990
  }
7931
7991
  function answersTo(entry, severity, contains) {
7932
7992
  if (!ADMITTED[severity].has(entry.severity)) {
@@ -7938,7 +7998,7 @@ function answersTo(entry, severity, contains) {
7938
7998
  const needle = contains.toLowerCase();
7939
7999
  return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
7940
8000
  }
7941
- var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED;
8001
+ var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED, FRAME;
7942
8002
  var init_game_log = __esm(() => {
7943
8003
  HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
7944
8004
  COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
@@ -7948,6 +8008,7 @@ var init_game_log = __esm(() => {
7948
8008
  warning: new Set(["error", "warning"]),
7949
8009
  info: new Set(["error", "warning", "info"])
7950
8010
  };
8011
+ FRAME = /^\[(\d+)\] (.+)$/;
7951
8012
  });
7952
8013
 
7953
8014
  // src/engine-run.ts
@@ -8166,7 +8227,7 @@ function problemsIn(log) {
8166
8227
  return [];
8167
8228
  });
8168
8229
  }
8169
- function reported(problems) {
8230
+ function reported2(problems) {
8170
8231
  const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
8171
8232
  return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
8172
8233
  }
@@ -8251,12 +8312,12 @@ async function importOnce(godotPath, projectPath) {
8251
8312
  }
8252
8313
  const problems = problemsIn(ran.log);
8253
8314
  if (ran.exitCode === 0 && ran.failure === null) {
8254
- return { ok: true, ...reported(problems) };
8315
+ return { ok: true, ...reported2(problems) };
8255
8316
  }
8256
8317
  return {
8257
8318
  ok: false,
8258
8319
  message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
8259
- ...reported(problems)
8320
+ ...reported2(problems)
8260
8321
  };
8261
8322
  }
8262
8323
  async function runOperation(engine, operation, params, projectPath) {
@@ -8290,7 +8351,7 @@ async function runOperation(engine, operation, params, projectPath) {
8290
8351
  return {
8291
8352
  ok: false,
8292
8353
  message: `${operation} wrote an answer that could not be read (${answer.why})`,
8293
- ...reported(problems)
8354
+ ...reported2(problems)
8294
8355
  };
8295
8356
  }
8296
8357
  if (answer.kind === "none") {
@@ -8298,7 +8359,7 @@ async function runOperation(engine, operation, params, projectPath) {
8298
8359
  return {
8299
8360
  ok: false,
8300
8361
  message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
8301
- ...reported(problems)
8362
+ ...reported2(problems)
8302
8363
  };
8303
8364
  }
8304
8365
  const fault = scriptFault(problems, engine.script);
@@ -8306,14 +8367,14 @@ async function runOperation(engine, operation, params, projectPath) {
8306
8367
  return {
8307
8368
  ok: false,
8308
8369
  message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
8309
- ...reported(problems)
8370
+ ...reported2(problems)
8310
8371
  };
8311
8372
  }
8312
8373
  const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
8313
8374
  return {
8314
8375
  ok: true,
8315
8376
  payload: answer.payload,
8316
- ...reported(problems),
8377
+ ...reported2(problems),
8317
8378
  ...afterAnswer === undefined ? {} : { afterAnswer }
8318
8379
  };
8319
8380
  }
@@ -17646,7 +17707,7 @@ var init_tool_definitions = __esm(() => {
17646
17707
  },
17647
17708
  {
17648
17709
  name: "project_test",
17649
- description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, 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. Needs gdUnit4 under addons/gdUnit4.",
17710
+ 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. 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.",
17650
17711
  parameters: {
17651
17712
  projectPath: PROJECT_PATH,
17652
17713
  path: {
@@ -18094,7 +18155,7 @@ var init_tool_definitions = __esm(() => {
18094
18155
  limit: {
18095
18156
  type: "integer",
18096
18157
  minimum: 1,
18097
- description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
18158
+ description: "The most entries to answer with, newest kept. Identical entries come back as one, at the first of them, with times saying how many were printed, so the limit counts distinct entries. Default 200, and omitted says how many distinct matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
18098
18159
  },
18099
18160
  before: {
18100
18161
  ops: ["editor"],
@@ -38515,7 +38576,7 @@ var init_godot_bridge = __esm(() => {
38515
38576
  case "godot_ready":
38516
38577
  if (this.connectionInfo) {
38517
38578
  if (this.belongsElsewhere(message.project_path)) {
38518
- this.sendElsewhere(message.project_path);
38579
+ this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server === true);
38519
38580
  return;
38520
38581
  }
38521
38582
  this.connectionInfo.projectPath = message.project_path;
@@ -38626,6 +38687,34 @@ var init_godot_bridge = __esm(() => {
38626
38687
  const fits = closeReason(said, "start") === said;
38627
38688
  stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
38628
38689
  }
38690
+ elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
38691
+ const known = pid === undefined ? undefined : this.oneShots.get(pid);
38692
+ if (pid !== undefined && known !== undefined) {
38693
+ this.turnAway(pid, known);
38694
+ return;
38695
+ }
38696
+ const info = this.connectionInfo;
38697
+ const socket = this.socket;
38698
+ const reader = this.oneShotReader;
38699
+ if (pid === undefined || openedByAServer || info === null || reader === null) {
38700
+ this.sendElsewhere(theirProject);
38701
+ return;
38702
+ }
38703
+ info.identifying = true;
38704
+ reader(pid).catch(() => {
38705
+ return;
38706
+ }).then((option) => {
38707
+ if (this.socket !== socket || this.connectionInfo !== info) {
38708
+ return;
38709
+ }
38710
+ if (typeof option === "string") {
38711
+ this.oneShots.set(pid, option);
38712
+ this.turnAway(pid, option);
38713
+ return;
38714
+ }
38715
+ this.sendElsewhere(theirProject);
38716
+ });
38717
+ }
38629
38718
  identify(pid) {
38630
38719
  const known = this.oneShots.get(pid);
38631
38720
  if (known !== undefined) {
@@ -42563,7 +42652,6 @@ function notRunNote(notRun, failFast) {
42563
42652
  function exportAnswer(ending, asked, before, after) {
42564
42653
  const { log } = ending;
42565
42654
  const written = after !== null && after !== before;
42566
- const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
42567
42655
  const verdict = {
42568
42656
  exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
42569
42657
  ...asked,
@@ -42572,8 +42660,7 @@ function exportAnswer(ending, asked, before, after) {
42572
42660
  failure: ending.failure ?? undefined,
42573
42661
  errors: log.count("error"),
42574
42662
  warnings: log.count("warning"),
42575
- entries: forAnswer(problems.entries),
42576
- ...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
42663
+ ...foldedEntriesAnswer(log)
42577
42664
  };
42578
42665
  if (verdict.exported) {
42579
42666
  return { content: [{ type: "text", text: answerJson(verdict) }] };
@@ -42779,6 +42866,32 @@ function aboveTheRunner(entry) {
42779
42866
  ]
42780
42867
  };
42781
42868
  }
42869
+ function foldedEntriesAnswer(log) {
42870
+ const problems = log.selectFolded({
42871
+ severity: "warning",
42872
+ sinceLastCall: false,
42873
+ limit: MOST_ENGINE_ENTRIES
42874
+ });
42875
+ return {
42876
+ entries: problems.entries,
42877
+ ...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
42878
+ };
42879
+ }
42880
+ function gdUnitFailsOnScriptErrors(projectPath) {
42881
+ try {
42882
+ return settingKeys(readFileSync22(join26(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
42883
+ } catch {
42884
+ return true;
42885
+ }
42886
+ }
42887
+ function uncaughtNote(uncaught) {
42888
+ const first = uncaught[0];
42889
+ if (first === undefined) {
42890
+ return;
42891
+ }
42892
+ const count = uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`;
42893
+ return `Every case passed, but the run printed ${count} that no case failed on, the first "${first.text}"; ` + "they are under uncaughtScriptErrors. gdUnit4 fails a case on the script errors its logger sees, and " + "it did not see these, so the cases passed on whatever the faulted calls returned. A stack overflow is " + "one it cannot see: it is raised deeper than the logger can be called.";
42894
+ }
42782
42895
  function elsewhereIn(projectPath, asked) {
42783
42896
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
42784
42897
  if (wanted === "" || existsSync20(join26(projectPath, wanted))) {
@@ -44563,7 +44676,8 @@ class GodotServer {
44563
44676
  rmdirSync2(dirname19(reportsDir));
44564
44677
  } catch {}
44565
44678
  }
44566
- const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
44679
+ const engineEntries = foldedRepeats(run.log.select({ severity: "warning", sinceLastCall: false, limit: Number.POSITIVE_INFINITY }).entries.map(aboveTheRunner));
44680
+ const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
44567
44681
  const verdicts = {
44568
44682
  0: "passed",
44569
44683
  100: "failures",
@@ -44573,6 +44687,7 @@ class GodotServer {
44573
44687
  105: "script errors"
44574
44688
  };
44575
44689
  const exitCode = run.exitCode;
44690
+ const uncaught = report !== null && exitCode === 0 && !timedOut && gdUnitFailsOnScriptErrors(project.value.path) ? run.log.all.filter((entry) => run.log.raisedByScript(entry) && !entry.detail.some((line) => line.includes(ERROR_ASSERT))) : [];
44576
44691
  const printed = run.log.select({
44577
44692
  severity: "info",
44578
44693
  sinceLastCall: false,
@@ -44586,7 +44701,7 @@ class GodotServer {
44586
44701
  const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
44587
44702
  const scriptErrors = [...declaring.origins, ...printedErrors];
44588
44703
  const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
44589
- const ranAs = 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));
44704
+ const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
44590
44705
  const nothingAt = missing.join(" and ");
44591
44706
  const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
44592
44707
  const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
@@ -44636,7 +44751,7 @@ class GodotServer {
44636
44751
  ...nonEmpty(foldedScriptErrors(scriptErrors)),
44637
44752
  ...atTheEnd,
44638
44753
  arguments: cmdArgs,
44639
- entries: forAnswer(withoutListed(printed, scriptErrors).slice(0, 60).map(aboveTheRunner)),
44754
+ entries: foldedRepeats(withoutListed(printed, scriptErrors).map(aboveTheRunner)).slice(0, 60),
44640
44755
  ...printedLines,
44641
44756
  savesNote,
44642
44757
  ...scanned
@@ -44671,7 +44786,7 @@ class GodotServer {
44671
44786
  });
44672
44787
  const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
44673
44788
  return this.jsonTextResponse({
44674
- passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
44789
+ passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
44675
44790
  verdict,
44676
44791
  ...missing.length > 0 ? { missing } : {},
44677
44792
  ...timedOut ? { timedOut, hung, silentForMs } : {},
@@ -44684,6 +44799,7 @@ class GodotServer {
44684
44799
  time: report.time,
44685
44800
  failed,
44686
44801
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
44802
+ uncaughtScriptErrors: uncaught.length > 0 ? foldedRepeats(uncaught.map(aboveTheRunner)) : undefined,
44687
44803
  warnings: warnings.length > 0 ? warnings : undefined,
44688
44804
  orphans: orphans.total > 0 ? orphans.total : undefined,
44689
44805
  notRun: notRun > 0 ? notRun : undefined,
@@ -44692,6 +44808,7 @@ class GodotServer {
44692
44808
  ...scanned,
44693
44809
  note: [
44694
44810
  missingNote,
44811
+ uncaughtNote(uncaught),
44695
44812
  ranAs.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." : notRunNote(notRun, readBoolean(args, "failFast") === true)
44696
44813
  ].filter((part) => part !== undefined).join(" ") || undefined,
44697
44814
  suites: unclean.map((suite) => ({
@@ -44709,7 +44826,8 @@ class GodotServer {
44709
44826
  suitesPassed: report.suites.length - unclean.length,
44710
44827
  engineErrors: run.log.count("error"),
44711
44828
  engineWarnings: run.log.count("warning"),
44712
- engineEntries,
44829
+ engineEntries: engineEntriesShown,
44830
+ engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
44713
44831
  classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
44714
44832
  });
44715
44833
  }
@@ -46418,7 +46536,7 @@ class GodotServer {
46418
46536
  warnings,
46419
46537
  ...scanWaitAnswer(scanned),
46420
46538
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
46421
- entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
46539
+ ...foldedEntriesAnswer(boot.log)
46422
46540
  });
46423
46541
  }
46424
46542
  async handleWaitForRun(args) {
@@ -46502,7 +46620,7 @@ class GodotServer {
46502
46620
  const asked = readString(args, "severity");
46503
46621
  const severity = asked === "error" || asked === "warning" ? asked : "info";
46504
46622
  const contains = readNonEmptyString(args, "contains");
46505
- const selected = console2.log.select({
46623
+ const selected = console2.log.selectFolded({
46506
46624
  severity,
46507
46625
  sinceLastCall: false,
46508
46626
  contains,
@@ -46531,7 +46649,7 @@ class GodotServer {
46531
46649
  },
46532
46650
  repeated: shown,
46533
46651
  moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
46534
- entries: forAnswer(selected.entries),
46652
+ entries: selected.entries,
46535
46653
  omitted: selected.omitted === 0 ? undefined : selected.omitted
46536
46654
  });
46537
46655
  }
@@ -46553,7 +46671,7 @@ class GodotServer {
46553
46671
  run.endedUnwatched = noCodeWillCome(run);
46554
46672
  }
46555
46673
  const severity = readString(args, "severity");
46556
- const selected = run.log.select({
46674
+ const selected = run.log.selectFolded({
46557
46675
  severity: severity === "error" || severity === "warning" ? severity : "info",
46558
46676
  sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
46559
46677
  contains: readNonEmptyString(args, "contains"),
@@ -46619,7 +46737,7 @@ class GodotServer {
46619
46737
  cpuSeconds,
46620
46738
  note: notes.length > 0 ? notes.join(" ") : undefined,
46621
46739
  omitted: selected.omitted,
46622
- entries: forAnswer(selected.entries)
46740
+ entries: selected.entries
46623
46741
  });
46624
46742
  }
46625
46743
  anotherProjectsRun(runsProject, args, act) {
@@ -46686,7 +46804,7 @@ class GodotServer {
46686
46804
  clean: verdict.clean,
46687
46805
  errorsUnread: errorsUnread(stopped) ? true : undefined,
46688
46806
  note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
46689
- entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
46807
+ ...foldedEntriesAnswer(stopped.log)
46690
46808
  });
46691
46809
  }
46692
46810
  async stopAnnouncedGame(pid, andChildren, args) {
@@ -47666,7 +47784,7 @@ async function runGodotServer() {
47666
47784
  const server = new GodotServer;
47667
47785
  await server.run();
47668
47786
  }
47669
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", EXTERNAL_MEMBER = 'Could not resolve external class member "';
47787
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
47670
47788
  var init_server3 = __esm(() => {
47671
47789
  init_mcp();
47672
47790
  init_stdio2();
package/build/index.js CHANGED
@@ -27850,7 +27850,11 @@ function announced(line) {
27850
27850
  if (!match) {
27851
27851
  return null;
27852
27852
  }
27853
- return { severity: match[2] === "WARNING" ? "warning" : "error", text: match[3] ?? "" };
27853
+ return {
27854
+ severity: match[2] === "WARNING" ? "warning" : "error",
27855
+ text: match[3] ?? "",
27856
+ byScript: match[2] === "SCRIPT ERROR"
27857
+ };
27854
27858
  }
27855
27859
  var STREAMS = ["stdout", "stderr", "transcript"];
27856
27860
  var ADMITTED = {
@@ -27858,8 +27862,49 @@ var ADMITTED = {
27858
27862
  warning: new Set(["error", "warning"]),
27859
27863
  info: new Set(["error", "warning", "info"])
27860
27864
  };
27861
- function forAnswer(entries) {
27862
- return entries.map((entry) => entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : entry);
27865
+ function reported(entry) {
27866
+ return entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : { ...entry, detail: framesCollapsed(entry.detail) };
27867
+ }
27868
+ var FRAME = /^\[(\d+)\] (.+)$/;
27869
+ function framesCollapsed(detail) {
27870
+ const out = [];
27871
+ let pending;
27872
+ for (const line of detail) {
27873
+ const match = FRAME.exec(line);
27874
+ const number = match?.[1];
27875
+ const frame = match?.[2];
27876
+ if (frame !== undefined && pending?.frame === frame && Number(number) === Number(pending.last) + 1) {
27877
+ pending.last = String(number);
27878
+ continue;
27879
+ }
27880
+ if (pending !== undefined) {
27881
+ out.push(writtenRun(pending));
27882
+ }
27883
+ pending = number === undefined || frame === undefined ? undefined : { first: number, last: number, frame };
27884
+ if (pending === undefined) {
27885
+ out.push(line);
27886
+ }
27887
+ }
27888
+ if (pending !== undefined) {
27889
+ out.push(writtenRun(pending));
27890
+ }
27891
+ return out;
27892
+ }
27893
+ function writtenRun(run) {
27894
+ return run.first === run.last ? `[${run.first}] ${run.frame}` : `[${run.first}-${run.last}] ${run.frame}`;
27895
+ }
27896
+ function foldedRepeats(entries) {
27897
+ const first = new Map;
27898
+ for (const entry of entries) {
27899
+ const key = JSON.stringify([entry.severity, entry.source, entry.text, entry.detail]);
27900
+ const seen = first.get(key);
27901
+ if (seen === undefined) {
27902
+ first.set(key, { entry, times: 1 });
27903
+ } else {
27904
+ seen.times += 1;
27905
+ }
27906
+ }
27907
+ return [...first.values()].map(({ entry, times }) => times === 1 ? reported(entry) : { ...reported(entry), times });
27863
27908
  }
27864
27909
 
27865
27910
  class GameLog {
@@ -27870,8 +27915,13 @@ class GameLog {
27870
27915
  transcript: new StringDecoder("utf8")
27871
27916
  };
27872
27917
  partial = { stdout: "", stderr: "", transcript: "" };
27873
- lastHeadline = null;
27918
+ lastHeadline = {
27919
+ stdout: null,
27920
+ stderr: null,
27921
+ transcript: null
27922
+ };
27874
27923
  seen = { error: 0, warning: 0, info: 0 };
27924
+ byScript = new Set;
27875
27925
  append(source, chunk) {
27876
27926
  const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
27877
27927
  const lines = (this.partial[source] + decoded).split(`
@@ -27895,8 +27945,9 @@ class GameLog {
27895
27945
  if (line.trim() === "") {
27896
27946
  return;
27897
27947
  }
27898
- if (this.lastHeadline && /^\s/.test(line) && this.lastHeadline.index === this.entries.length - 1) {
27899
- this.lastHeadline.detail.push(line.trim());
27948
+ const owner = this.lastHeadline[source];
27949
+ if (owner !== null && /^\s/.test(line)) {
27950
+ owner.detail.push(line.trim());
27900
27951
  return;
27901
27952
  }
27902
27953
  const headline = announced(line);
@@ -27909,7 +27960,13 @@ class GameLog {
27909
27960
  detail
27910
27961
  };
27911
27962
  this.entries.push(entry);
27912
- this.lastHeadline = headline ? { index: entry.index, detail } : null;
27963
+ if (headline?.byScript === true) {
27964
+ this.byScript.add(entry.index);
27965
+ }
27966
+ this.lastHeadline[source] = headline ? { detail } : null;
27967
+ }
27968
+ raisedByScript(entry) {
27969
+ return this.byScript.has(entry.index);
27913
27970
  }
27914
27971
  record(severity, text) {
27915
27972
  this.entries.push({
@@ -27919,7 +27976,6 @@ class GameLog {
27919
27976
  text,
27920
27977
  detail: []
27921
27978
  });
27922
- this.lastHeadline = null;
27923
27979
  }
27924
27980
  get all() {
27925
27981
  return this.entries;
@@ -27943,6 +27999,11 @@ class GameLog {
27943
27999
  const omitted = Math.max(0, matching.length - options.limit);
27944
28000
  return { entries: matching.slice(omitted), omitted };
27945
28001
  }
28002
+ selectFolded(options) {
28003
+ const folded = foldedRepeats(this.select({ ...options, limit: Number.POSITIVE_INFINITY }).entries);
28004
+ const omitted = Math.max(0, folded.length - options.limit);
28005
+ return { entries: folded.slice(omitted), omitted };
28006
+ }
27946
28007
  }
27947
28008
  function answersTo(entry, severity, contains) {
27948
28009
  if (!ADMITTED[severity].has(entry.severity)) {
@@ -28570,7 +28631,7 @@ class GodotBridge extends EventEmitter {
28570
28631
  case "godot_ready":
28571
28632
  if (this.connectionInfo) {
28572
28633
  if (this.belongsElsewhere(message.project_path)) {
28573
- this.sendElsewhere(message.project_path);
28634
+ this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server === true);
28574
28635
  return;
28575
28636
  }
28576
28637
  this.connectionInfo.projectPath = message.project_path;
@@ -28681,6 +28742,34 @@ class GodotBridge extends EventEmitter {
28681
28742
  const fits = closeReason(said, "start") === said;
28682
28743
  stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
28683
28744
  }
28745
+ elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
28746
+ const known = pid === undefined ? undefined : this.oneShots.get(pid);
28747
+ if (pid !== undefined && known !== undefined) {
28748
+ this.turnAway(pid, known);
28749
+ return;
28750
+ }
28751
+ const info = this.connectionInfo;
28752
+ const socket = this.socket;
28753
+ const reader = this.oneShotReader;
28754
+ if (pid === undefined || openedByAServer || info === null || reader === null) {
28755
+ this.sendElsewhere(theirProject);
28756
+ return;
28757
+ }
28758
+ info.identifying = true;
28759
+ reader(pid).catch(() => {
28760
+ return;
28761
+ }).then((option) => {
28762
+ if (this.socket !== socket || this.connectionInfo !== info) {
28763
+ return;
28764
+ }
28765
+ if (typeof option === "string") {
28766
+ this.oneShots.set(pid, option);
28767
+ this.turnAway(pid, option);
28768
+ return;
28769
+ }
28770
+ this.sendElsewhere(theirProject);
28771
+ });
28772
+ }
28684
28773
  identify(pid) {
28685
28774
  const known = this.oneShots.get(pid);
28686
28775
  if (known !== undefined) {
@@ -29118,7 +29207,7 @@ function problemsIn(log) {
29118
29207
  return [];
29119
29208
  });
29120
29209
  }
29121
- function reported(problems) {
29210
+ function reported2(problems) {
29122
29211
  const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
29123
29212
  return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
29124
29213
  }
@@ -29203,12 +29292,12 @@ async function importOnce(godotPath, projectPath) {
29203
29292
  }
29204
29293
  const problems = problemsIn(ran.log);
29205
29294
  if (ran.exitCode === 0 && ran.failure === null) {
29206
- return { ok: true, ...reported(problems) };
29295
+ return { ok: true, ...reported2(problems) };
29207
29296
  }
29208
29297
  return {
29209
29298
  ok: false,
29210
29299
  message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
29211
- ...reported(problems)
29300
+ ...reported2(problems)
29212
29301
  };
29213
29302
  }
29214
29303
  async function runOperation(engine, operation, params, projectPath) {
@@ -29242,7 +29331,7 @@ async function runOperation(engine, operation, params, projectPath) {
29242
29331
  return {
29243
29332
  ok: false,
29244
29333
  message: `${operation} wrote an answer that could not be read (${answer.why})`,
29245
- ...reported(problems)
29334
+ ...reported2(problems)
29246
29335
  };
29247
29336
  }
29248
29337
  if (answer.kind === "none") {
@@ -29250,7 +29339,7 @@ async function runOperation(engine, operation, params, projectPath) {
29250
29339
  return {
29251
29340
  ok: false,
29252
29341
  message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
29253
- ...reported(problems)
29342
+ ...reported2(problems)
29254
29343
  };
29255
29344
  }
29256
29345
  const fault = scriptFault(problems, engine.script);
@@ -29258,14 +29347,14 @@ async function runOperation(engine, operation, params, projectPath) {
29258
29347
  return {
29259
29348
  ok: false,
29260
29349
  message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
29261
- ...reported(problems)
29350
+ ...reported2(problems)
29262
29351
  };
29263
29352
  }
29264
29353
  const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
29265
29354
  return {
29266
29355
  ok: true,
29267
29356
  payload: answer.payload,
29268
- ...reported(problems),
29357
+ ...reported2(problems),
29269
29358
  ...afterAnswer === undefined ? {} : { afterAnswer }
29270
29359
  };
29271
29360
  }
@@ -33500,7 +33589,7 @@ var TOOL_SPECS = [
33500
33589
  },
33501
33590
  {
33502
33591
  name: "project_test",
33503
- description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, 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. Needs gdUnit4 under addons/gdUnit4.",
33592
+ 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. 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.",
33504
33593
  parameters: {
33505
33594
  projectPath: PROJECT_PATH,
33506
33595
  path: {
@@ -33948,7 +34037,7 @@ var TOOL_SPECS = [
33948
34037
  limit: {
33949
34038
  type: "integer",
33950
34039
  minimum: 1,
33951
- description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
34040
+ description: "The most entries to answer with, newest kept. Identical entries come back as one, at the first of them, with times saying how many were printed, so the limit counts distinct entries. Default 200, and omitted says how many distinct matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
33952
34041
  },
33953
34042
  before: {
33954
34043
  ops: ["editor"],
@@ -35010,7 +35099,6 @@ function notRunNote(notRun, failFast) {
35010
35099
  function exportAnswer(ending, asked, before, after) {
35011
35100
  const { log } = ending;
35012
35101
  const written = after !== null && after !== before;
35013
- const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
35014
35102
  const verdict = {
35015
35103
  exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
35016
35104
  ...asked,
@@ -35019,8 +35107,7 @@ function exportAnswer(ending, asked, before, after) {
35019
35107
  failure: ending.failure ?? undefined,
35020
35108
  errors: log.count("error"),
35021
35109
  warnings: log.count("warning"),
35022
- entries: forAnswer(problems.entries),
35023
- ...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
35110
+ ...foldedEntriesAnswer(log)
35024
35111
  };
35025
35112
  if (verdict.exported) {
35026
35113
  return { content: [{ type: "text", text: answerJson(verdict) }] };
@@ -35270,6 +35357,34 @@ function aboveTheRunner(entry) {
35270
35357
  ]
35271
35358
  };
35272
35359
  }
35360
+ var MOST_ENGINE_ENTRIES = 200;
35361
+ function foldedEntriesAnswer(log) {
35362
+ const problems = log.selectFolded({
35363
+ severity: "warning",
35364
+ sinceLastCall: false,
35365
+ limit: MOST_ENGINE_ENTRIES
35366
+ });
35367
+ return {
35368
+ entries: problems.entries,
35369
+ ...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
35370
+ };
35371
+ }
35372
+ var ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd";
35373
+ function gdUnitFailsOnScriptErrors(projectPath) {
35374
+ try {
35375
+ return settingKeys(readFileSync20(join22(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
35376
+ } catch {
35377
+ return true;
35378
+ }
35379
+ }
35380
+ function uncaughtNote(uncaught) {
35381
+ const first = uncaught[0];
35382
+ if (first === undefined) {
35383
+ return;
35384
+ }
35385
+ const count = uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`;
35386
+ return `Every case passed, but the run printed ${count} that no case failed on, the first "${first.text}"; ` + "they are under uncaughtScriptErrors. gdUnit4 fails a case on the script errors its logger sees, and " + "it did not see these, so the cases passed on whatever the faulted calls returned. A stack overflow is " + "one it cannot see: it is raised deeper than the logger can be called.";
35387
+ }
35273
35388
  function elsewhereIn(projectPath, asked) {
35274
35389
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
35275
35390
  if (wanted === "" || existsSync17(join22(projectPath, wanted))) {
@@ -37055,7 +37170,8 @@ class GodotServer {
37055
37170
  rmdirSync2(dirname15(reportsDir));
37056
37171
  } catch {}
37057
37172
  }
37058
- const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
37173
+ const engineEntries = foldedRepeats(run.log.select({ severity: "warning", sinceLastCall: false, limit: Number.POSITIVE_INFINITY }).entries.map(aboveTheRunner));
37174
+ const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
37059
37175
  const verdicts = {
37060
37176
  0: "passed",
37061
37177
  100: "failures",
@@ -37065,6 +37181,7 @@ class GodotServer {
37065
37181
  105: "script errors"
37066
37182
  };
37067
37183
  const exitCode = run.exitCode;
37184
+ const uncaught = report !== null && exitCode === 0 && !timedOut && gdUnitFailsOnScriptErrors(project.value.path) ? run.log.all.filter((entry) => run.log.raisedByScript(entry) && !entry.detail.some((line) => line.includes(ERROR_ASSERT))) : [];
37068
37185
  const printed = run.log.select({
37069
37186
  severity: "info",
37070
37187
  sinceLastCall: false,
@@ -37078,7 +37195,7 @@ class GodotServer {
37078
37195
  const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
37079
37196
  const scriptErrors = [...declaring.origins, ...printedErrors];
37080
37197
  const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
37081
- const ranAs = 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));
37198
+ const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
37082
37199
  const nothingAt = missing.join(" and ");
37083
37200
  const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
37084
37201
  const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
@@ -37128,7 +37245,7 @@ class GodotServer {
37128
37245
  ...nonEmpty(foldedScriptErrors(scriptErrors)),
37129
37246
  ...atTheEnd,
37130
37247
  arguments: cmdArgs,
37131
- entries: forAnswer(withoutListed(printed, scriptErrors).slice(0, 60).map(aboveTheRunner)),
37248
+ entries: foldedRepeats(withoutListed(printed, scriptErrors).map(aboveTheRunner)).slice(0, 60),
37132
37249
  ...printedLines,
37133
37250
  savesNote,
37134
37251
  ...scanned
@@ -37163,7 +37280,7 @@ class GodotServer {
37163
37280
  });
37164
37281
  const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
37165
37282
  return this.jsonTextResponse({
37166
- passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
37283
+ passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
37167
37284
  verdict,
37168
37285
  ...missing.length > 0 ? { missing } : {},
37169
37286
  ...timedOut ? { timedOut, hung, silentForMs } : {},
@@ -37176,6 +37293,7 @@ class GodotServer {
37176
37293
  time: report.time,
37177
37294
  failed,
37178
37295
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
37296
+ uncaughtScriptErrors: uncaught.length > 0 ? foldedRepeats(uncaught.map(aboveTheRunner)) : undefined,
37179
37297
  warnings: warnings.length > 0 ? warnings : undefined,
37180
37298
  orphans: orphans.total > 0 ? orphans.total : undefined,
37181
37299
  notRun: notRun > 0 ? notRun : undefined,
@@ -37184,6 +37302,7 @@ class GodotServer {
37184
37302
  ...scanned,
37185
37303
  note: [
37186
37304
  missingNote,
37305
+ uncaughtNote(uncaught),
37187
37306
  ranAs.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." : notRunNote(notRun, readBoolean(args, "failFast") === true)
37188
37307
  ].filter((part) => part !== undefined).join(" ") || undefined,
37189
37308
  suites: unclean.map((suite) => ({
@@ -37201,7 +37320,8 @@ class GodotServer {
37201
37320
  suitesPassed: report.suites.length - unclean.length,
37202
37321
  engineErrors: run.log.count("error"),
37203
37322
  engineWarnings: run.log.count("warning"),
37204
- engineEntries,
37323
+ engineEntries: engineEntriesShown,
37324
+ engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
37205
37325
  classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
37206
37326
  });
37207
37327
  }
@@ -38910,7 +39030,7 @@ class GodotServer {
38910
39030
  warnings,
38911
39031
  ...scanWaitAnswer(scanned),
38912
39032
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
38913
- entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
39033
+ ...foldedEntriesAnswer(boot.log)
38914
39034
  });
38915
39035
  }
38916
39036
  async handleWaitForRun(args) {
@@ -38994,7 +39114,7 @@ class GodotServer {
38994
39114
  const asked = readString(args, "severity");
38995
39115
  const severity = asked === "error" || asked === "warning" ? asked : "info";
38996
39116
  const contains = readNonEmptyString(args, "contains");
38997
- const selected = console2.log.select({
39117
+ const selected = console2.log.selectFolded({
38998
39118
  severity,
38999
39119
  sinceLastCall: false,
39000
39120
  contains,
@@ -39023,7 +39143,7 @@ class GodotServer {
39023
39143
  },
39024
39144
  repeated: shown,
39025
39145
  moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
39026
- entries: forAnswer(selected.entries),
39146
+ entries: selected.entries,
39027
39147
  omitted: selected.omitted === 0 ? undefined : selected.omitted
39028
39148
  });
39029
39149
  }
@@ -39045,7 +39165,7 @@ class GodotServer {
39045
39165
  run.endedUnwatched = noCodeWillCome(run);
39046
39166
  }
39047
39167
  const severity = readString(args, "severity");
39048
- const selected = run.log.select({
39168
+ const selected = run.log.selectFolded({
39049
39169
  severity: severity === "error" || severity === "warning" ? severity : "info",
39050
39170
  sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
39051
39171
  contains: readNonEmptyString(args, "contains"),
@@ -39111,7 +39231,7 @@ class GodotServer {
39111
39231
  cpuSeconds,
39112
39232
  note: notes.length > 0 ? notes.join(" ") : undefined,
39113
39233
  omitted: selected.omitted,
39114
- entries: forAnswer(selected.entries)
39234
+ entries: selected.entries
39115
39235
  });
39116
39236
  }
39117
39237
  anotherProjectsRun(runsProject, args, act) {
@@ -39178,7 +39298,7 @@ class GodotServer {
39178
39298
  clean: verdict.clean,
39179
39299
  errorsUnread: errorsUnread(stopped) ? true : undefined,
39180
39300
  note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
39181
- entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
39301
+ ...foldedEntriesAnswer(stopped.log)
39182
39302
  });
39183
39303
  }
39184
39304
  async stopAnnouncedGame(pid, andChildren, args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.54",
3
+ "version": "1.1.56",
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",