gdharness 1.1.55 → 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 +123 -33
  2. package/build/index.js +123 -31
  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;
7875
+ }
7876
+ function writtenRun(run) {
7877
+ return run.first === run.last ? `[${run.first}] ${run.frame}` : `[${run.first}-${run.last}] ${run.frame}`;
7844
7878
  }
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);
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"],
@@ -42591,7 +42652,6 @@ function notRunNote(notRun, failFast) {
42591
42652
  function exportAnswer(ending, asked, before, after) {
42592
42653
  const { log } = ending;
42593
42654
  const written = after !== null && after !== before;
42594
- const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
42595
42655
  const verdict = {
42596
42656
  exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
42597
42657
  ...asked,
@@ -42600,8 +42660,7 @@ function exportAnswer(ending, asked, before, after) {
42600
42660
  failure: ending.failure ?? undefined,
42601
42661
  errors: log.count("error"),
42602
42662
  warnings: log.count("warning"),
42603
- entries: forAnswer(problems.entries),
42604
- ...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
42663
+ ...foldedEntriesAnswer(log)
42605
42664
  };
42606
42665
  if (verdict.exported) {
42607
42666
  return { content: [{ type: "text", text: answerJson(verdict) }] };
@@ -42807,6 +42866,32 @@ function aboveTheRunner(entry) {
42807
42866
  ]
42808
42867
  };
42809
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
+ }
42810
42895
  function elsewhereIn(projectPath, asked) {
42811
42896
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
42812
42897
  if (wanted === "" || existsSync20(join26(projectPath, wanted))) {
@@ -44591,7 +44676,8 @@ class GodotServer {
44591
44676
  rmdirSync2(dirname19(reportsDir));
44592
44677
  } catch {}
44593
44678
  }
44594
- 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);
44595
44681
  const verdicts = {
44596
44682
  0: "passed",
44597
44683
  100: "failures",
@@ -44601,6 +44687,7 @@ class GodotServer {
44601
44687
  105: "script errors"
44602
44688
  };
44603
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))) : [];
44604
44691
  const printed = run.log.select({
44605
44692
  severity: "info",
44606
44693
  sinceLastCall: false,
@@ -44614,7 +44701,7 @@ class GodotServer {
44614
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: [] };
44615
44702
  const scriptErrors = [...declaring.origins, ...printedErrors];
44616
44703
  const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
44617
- 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));
44618
44705
  const nothingAt = missing.join(" and ");
44619
44706
  const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
44620
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] ?? "")}`;
@@ -44664,7 +44751,7 @@ class GodotServer {
44664
44751
  ...nonEmpty(foldedScriptErrors(scriptErrors)),
44665
44752
  ...atTheEnd,
44666
44753
  arguments: cmdArgs,
44667
- entries: forAnswer(withoutListed(printed, scriptErrors).slice(0, 60).map(aboveTheRunner)),
44754
+ entries: foldedRepeats(withoutListed(printed, scriptErrors).map(aboveTheRunner)).slice(0, 60),
44668
44755
  ...printedLines,
44669
44756
  savesNote,
44670
44757
  ...scanned
@@ -44699,7 +44786,7 @@ class GodotServer {
44699
44786
  });
44700
44787
  const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
44701
44788
  return this.jsonTextResponse({
44702
- 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,
44703
44790
  verdict,
44704
44791
  ...missing.length > 0 ? { missing } : {},
44705
44792
  ...timedOut ? { timedOut, hung, silentForMs } : {},
@@ -44712,6 +44799,7 @@ class GodotServer {
44712
44799
  time: report.time,
44713
44800
  failed,
44714
44801
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
44802
+ uncaughtScriptErrors: uncaught.length > 0 ? foldedRepeats(uncaught.map(aboveTheRunner)) : undefined,
44715
44803
  warnings: warnings.length > 0 ? warnings : undefined,
44716
44804
  orphans: orphans.total > 0 ? orphans.total : undefined,
44717
44805
  notRun: notRun > 0 ? notRun : undefined,
@@ -44720,6 +44808,7 @@ class GodotServer {
44720
44808
  ...scanned,
44721
44809
  note: [
44722
44810
  missingNote,
44811
+ uncaughtNote(uncaught),
44723
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)
44724
44813
  ].filter((part) => part !== undefined).join(" ") || undefined,
44725
44814
  suites: unclean.map((suite) => ({
@@ -44737,7 +44826,8 @@ class GodotServer {
44737
44826
  suitesPassed: report.suites.length - unclean.length,
44738
44827
  engineErrors: run.log.count("error"),
44739
44828
  engineWarnings: run.log.count("warning"),
44740
- engineEntries,
44829
+ engineEntries: engineEntriesShown,
44830
+ engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
44741
44831
  classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
44742
44832
  });
44743
44833
  }
@@ -46446,7 +46536,7 @@ class GodotServer {
46446
46536
  warnings,
46447
46537
  ...scanWaitAnswer(scanned),
46448
46538
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
46449
- entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
46539
+ ...foldedEntriesAnswer(boot.log)
46450
46540
  });
46451
46541
  }
46452
46542
  async handleWaitForRun(args) {
@@ -46530,7 +46620,7 @@ class GodotServer {
46530
46620
  const asked = readString(args, "severity");
46531
46621
  const severity = asked === "error" || asked === "warning" ? asked : "info";
46532
46622
  const contains = readNonEmptyString(args, "contains");
46533
- const selected = console2.log.select({
46623
+ const selected = console2.log.selectFolded({
46534
46624
  severity,
46535
46625
  sinceLastCall: false,
46536
46626
  contains,
@@ -46559,7 +46649,7 @@ class GodotServer {
46559
46649
  },
46560
46650
  repeated: shown,
46561
46651
  moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
46562
- entries: forAnswer(selected.entries),
46652
+ entries: selected.entries,
46563
46653
  omitted: selected.omitted === 0 ? undefined : selected.omitted
46564
46654
  });
46565
46655
  }
@@ -46581,7 +46671,7 @@ class GodotServer {
46581
46671
  run.endedUnwatched = noCodeWillCome(run);
46582
46672
  }
46583
46673
  const severity = readString(args, "severity");
46584
- const selected = run.log.select({
46674
+ const selected = run.log.selectFolded({
46585
46675
  severity: severity === "error" || severity === "warning" ? severity : "info",
46586
46676
  sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
46587
46677
  contains: readNonEmptyString(args, "contains"),
@@ -46647,7 +46737,7 @@ class GodotServer {
46647
46737
  cpuSeconds,
46648
46738
  note: notes.length > 0 ? notes.join(" ") : undefined,
46649
46739
  omitted: selected.omitted,
46650
- entries: forAnswer(selected.entries)
46740
+ entries: selected.entries
46651
46741
  });
46652
46742
  }
46653
46743
  anotherProjectsRun(runsProject, args, act) {
@@ -46714,7 +46804,7 @@ class GodotServer {
46714
46804
  clean: verdict.clean,
46715
46805
  errorsUnread: errorsUnread(stopped) ? true : undefined,
46716
46806
  note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
46717
- entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
46807
+ ...foldedEntriesAnswer(stopped.log)
46718
46808
  });
46719
46809
  }
46720
46810
  async stopAnnouncedGame(pid, andChildren, args) {
@@ -47694,7 +47784,7 @@ async function runGodotServer() {
47694
47784
  const server = new GodotServer;
47695
47785
  await server.run();
47696
47786
  }
47697
- 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 "';
47698
47788
  var init_server3 = __esm(() => {
47699
47789
  init_mcp();
47700
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)) {
@@ -29146,7 +29207,7 @@ function problemsIn(log) {
29146
29207
  return [];
29147
29208
  });
29148
29209
  }
29149
- function reported(problems) {
29210
+ function reported2(problems) {
29150
29211
  const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
29151
29212
  return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
29152
29213
  }
@@ -29231,12 +29292,12 @@ async function importOnce(godotPath, projectPath) {
29231
29292
  }
29232
29293
  const problems = problemsIn(ran.log);
29233
29294
  if (ran.exitCode === 0 && ran.failure === null) {
29234
- return { ok: true, ...reported(problems) };
29295
+ return { ok: true, ...reported2(problems) };
29235
29296
  }
29236
29297
  return {
29237
29298
  ok: false,
29238
29299
  message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
29239
- ...reported(problems)
29300
+ ...reported2(problems)
29240
29301
  };
29241
29302
  }
29242
29303
  async function runOperation(engine, operation, params, projectPath) {
@@ -29270,7 +29331,7 @@ async function runOperation(engine, operation, params, projectPath) {
29270
29331
  return {
29271
29332
  ok: false,
29272
29333
  message: `${operation} wrote an answer that could not be read (${answer.why})`,
29273
- ...reported(problems)
29334
+ ...reported2(problems)
29274
29335
  };
29275
29336
  }
29276
29337
  if (answer.kind === "none") {
@@ -29278,7 +29339,7 @@ async function runOperation(engine, operation, params, projectPath) {
29278
29339
  return {
29279
29340
  ok: false,
29280
29341
  message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
29281
- ...reported(problems)
29342
+ ...reported2(problems)
29282
29343
  };
29283
29344
  }
29284
29345
  const fault = scriptFault(problems, engine.script);
@@ -29286,14 +29347,14 @@ async function runOperation(engine, operation, params, projectPath) {
29286
29347
  return {
29287
29348
  ok: false,
29288
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"})`,
29289
- ...reported(problems)
29350
+ ...reported2(problems)
29290
29351
  };
29291
29352
  }
29292
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.`;
29293
29354
  return {
29294
29355
  ok: true,
29295
29356
  payload: answer.payload,
29296
- ...reported(problems),
29357
+ ...reported2(problems),
29297
29358
  ...afterAnswer === undefined ? {} : { afterAnswer }
29298
29359
  };
29299
29360
  }
@@ -33528,7 +33589,7 @@ var TOOL_SPECS = [
33528
33589
  },
33529
33590
  {
33530
33591
  name: "project_test",
33531
- 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.",
33532
33593
  parameters: {
33533
33594
  projectPath: PROJECT_PATH,
33534
33595
  path: {
@@ -33976,7 +34037,7 @@ var TOOL_SPECS = [
33976
34037
  limit: {
33977
34038
  type: "integer",
33978
34039
  minimum: 1,
33979
- 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."
33980
34041
  },
33981
34042
  before: {
33982
34043
  ops: ["editor"],
@@ -35038,7 +35099,6 @@ function notRunNote(notRun, failFast) {
35038
35099
  function exportAnswer(ending, asked, before, after) {
35039
35100
  const { log } = ending;
35040
35101
  const written = after !== null && after !== before;
35041
- const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
35042
35102
  const verdict = {
35043
35103
  exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
35044
35104
  ...asked,
@@ -35047,8 +35107,7 @@ function exportAnswer(ending, asked, before, after) {
35047
35107
  failure: ending.failure ?? undefined,
35048
35108
  errors: log.count("error"),
35049
35109
  warnings: log.count("warning"),
35050
- entries: forAnswer(problems.entries),
35051
- ...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
35110
+ ...foldedEntriesAnswer(log)
35052
35111
  };
35053
35112
  if (verdict.exported) {
35054
35113
  return { content: [{ type: "text", text: answerJson(verdict) }] };
@@ -35298,6 +35357,34 @@ function aboveTheRunner(entry) {
35298
35357
  ]
35299
35358
  };
35300
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
+ }
35301
35388
  function elsewhereIn(projectPath, asked) {
35302
35389
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
35303
35390
  if (wanted === "" || existsSync17(join22(projectPath, wanted))) {
@@ -37083,7 +37170,8 @@ class GodotServer {
37083
37170
  rmdirSync2(dirname15(reportsDir));
37084
37171
  } catch {}
37085
37172
  }
37086
- 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);
37087
37175
  const verdicts = {
37088
37176
  0: "passed",
37089
37177
  100: "failures",
@@ -37093,6 +37181,7 @@ class GodotServer {
37093
37181
  105: "script errors"
37094
37182
  };
37095
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))) : [];
37096
37185
  const printed = run.log.select({
37097
37186
  severity: "info",
37098
37187
  sinceLastCall: false,
@@ -37106,7 +37195,7 @@ class GodotServer {
37106
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: [] };
37107
37196
  const scriptErrors = [...declaring.origins, ...printedErrors];
37108
37197
  const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
37109
- 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));
37110
37199
  const nothingAt = missing.join(" and ");
37111
37200
  const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
37112
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] ?? "")}`;
@@ -37156,7 +37245,7 @@ class GodotServer {
37156
37245
  ...nonEmpty(foldedScriptErrors(scriptErrors)),
37157
37246
  ...atTheEnd,
37158
37247
  arguments: cmdArgs,
37159
- entries: forAnswer(withoutListed(printed, scriptErrors).slice(0, 60).map(aboveTheRunner)),
37248
+ entries: foldedRepeats(withoutListed(printed, scriptErrors).map(aboveTheRunner)).slice(0, 60),
37160
37249
  ...printedLines,
37161
37250
  savesNote,
37162
37251
  ...scanned
@@ -37191,7 +37280,7 @@ class GodotServer {
37191
37280
  });
37192
37281
  const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
37193
37282
  return this.jsonTextResponse({
37194
- 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,
37195
37284
  verdict,
37196
37285
  ...missing.length > 0 ? { missing } : {},
37197
37286
  ...timedOut ? { timedOut, hung, silentForMs } : {},
@@ -37204,6 +37293,7 @@ class GodotServer {
37204
37293
  time: report.time,
37205
37294
  failed,
37206
37295
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
37296
+ uncaughtScriptErrors: uncaught.length > 0 ? foldedRepeats(uncaught.map(aboveTheRunner)) : undefined,
37207
37297
  warnings: warnings.length > 0 ? warnings : undefined,
37208
37298
  orphans: orphans.total > 0 ? orphans.total : undefined,
37209
37299
  notRun: notRun > 0 ? notRun : undefined,
@@ -37212,6 +37302,7 @@ class GodotServer {
37212
37302
  ...scanned,
37213
37303
  note: [
37214
37304
  missingNote,
37305
+ uncaughtNote(uncaught),
37215
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)
37216
37307
  ].filter((part) => part !== undefined).join(" ") || undefined,
37217
37308
  suites: unclean.map((suite) => ({
@@ -37229,7 +37320,8 @@ class GodotServer {
37229
37320
  suitesPassed: report.suites.length - unclean.length,
37230
37321
  engineErrors: run.log.count("error"),
37231
37322
  engineWarnings: run.log.count("warning"),
37232
- engineEntries,
37323
+ engineEntries: engineEntriesShown,
37324
+ engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
37233
37325
  classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
37234
37326
  });
37235
37327
  }
@@ -38938,7 +39030,7 @@ class GodotServer {
38938
39030
  warnings,
38939
39031
  ...scanWaitAnswer(scanned),
38940
39032
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
38941
- entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
39033
+ ...foldedEntriesAnswer(boot.log)
38942
39034
  });
38943
39035
  }
38944
39036
  async handleWaitForRun(args) {
@@ -39022,7 +39114,7 @@ class GodotServer {
39022
39114
  const asked = readString(args, "severity");
39023
39115
  const severity = asked === "error" || asked === "warning" ? asked : "info";
39024
39116
  const contains = readNonEmptyString(args, "contains");
39025
- const selected = console2.log.select({
39117
+ const selected = console2.log.selectFolded({
39026
39118
  severity,
39027
39119
  sinceLastCall: false,
39028
39120
  contains,
@@ -39051,7 +39143,7 @@ class GodotServer {
39051
39143
  },
39052
39144
  repeated: shown,
39053
39145
  moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
39054
- entries: forAnswer(selected.entries),
39146
+ entries: selected.entries,
39055
39147
  omitted: selected.omitted === 0 ? undefined : selected.omitted
39056
39148
  });
39057
39149
  }
@@ -39073,7 +39165,7 @@ class GodotServer {
39073
39165
  run.endedUnwatched = noCodeWillCome(run);
39074
39166
  }
39075
39167
  const severity = readString(args, "severity");
39076
- const selected = run.log.select({
39168
+ const selected = run.log.selectFolded({
39077
39169
  severity: severity === "error" || severity === "warning" ? severity : "info",
39078
39170
  sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
39079
39171
  contains: readNonEmptyString(args, "contains"),
@@ -39139,7 +39231,7 @@ class GodotServer {
39139
39231
  cpuSeconds,
39140
39232
  note: notes.length > 0 ? notes.join(" ") : undefined,
39141
39233
  omitted: selected.omitted,
39142
- entries: forAnswer(selected.entries)
39234
+ entries: selected.entries
39143
39235
  });
39144
39236
  }
39145
39237
  anotherProjectsRun(runsProject, args, act) {
@@ -39206,7 +39298,7 @@ class GodotServer {
39206
39298
  clean: verdict.clean,
39207
39299
  errorsUnread: errorsUnread(stopped) ? true : undefined,
39208
39300
  note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
39209
- entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
39301
+ ...foldedEntriesAnswer(stopped.log)
39210
39302
  });
39211
39303
  }
39212
39304
  async stopAnnouncedGame(pid, andChildren, args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.55",
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",