gdharness 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -15393,7 +15393,7 @@ var init_tool_definitions = __esm(() => {
15393
15393
  },
15394
15394
  {
15395
15395
  name: "project_test",
15396
- 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. 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. Needs gdUnit4 under addons/gdUnit4.",
15396
+ 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. 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. 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. Needs gdUnit4 under addons/gdUnit4.",
15397
15397
  parameters: {
15398
15398
  projectPath: PROJECT_PATH,
15399
15399
  path: {
@@ -15668,7 +15668,7 @@ var init_tool_definitions = __esm(() => {
15668
15668
  },
15669
15669
  {
15670
15670
  name: "editor_run",
15671
- description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until stop, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
15671
+ description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
15672
15672
  parameters: {
15673
15673
  projectPath: PROJECT_PATH,
15674
15674
  scene: { type: "string", description: "A scene to run instead of the main scene." },
@@ -15681,7 +15681,7 @@ var init_tool_definitions = __esm(() => {
15681
15681
  },
15682
15682
  requires: ["projectPath"],
15683
15683
  operations: {
15684
- start: { summary: "run the project until stop", requires: [] },
15684
+ start: { summary: "run the project until it quits or is stopped", requires: [] },
15685
15685
  stop: { summary: "end the run and answer with what it printed last", requires: [] },
15686
15686
  check: { summary: "boot headless, quit after a few frames, and report the verdict", requires: [] }
15687
15687
  },
@@ -15689,7 +15689,7 @@ var init_tool_definitions = __esm(() => {
15689
15689
  },
15690
15690
  {
15691
15691
  name: "editor_output",
15692
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries.",
15692
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts.",
15693
15693
  parameters: {
15694
15694
  severity: {
15695
15695
  type: "string",
@@ -35141,7 +35141,14 @@ function parseJUnit(xml) {
35141
35141
  suites
35142
35142
  };
35143
35143
  }
35144
- var MalformedReportError, ENTITIES, ATTRIBUTE;
35144
+ function whyNoReport(printed, asked) {
35145
+ const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
35146
+ if (missing !== undefined) {
35147
+ return `nothing at ${missing[1] ?? asked}`;
35148
+ }
35149
+ return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
35150
+ }
35151
+ var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH;
35145
35152
  var init_junit = __esm(() => {
35146
35153
  MalformedReportError = class MalformedReportError extends Error {
35147
35154
  constructor(message) {
@@ -35151,6 +35158,8 @@ var init_junit = __esm(() => {
35151
35158
  };
35152
35159
  ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
35153
35160
  ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
35161
+ NOTHING_RAN = /no test cases found/i;
35162
+ NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
35154
35163
  });
35155
35164
 
35156
35165
  // src/lsp_client.ts
@@ -36084,6 +36093,21 @@ function aboveTheRunner(entry) {
36084
36093
  detail: [...entry.detail.slice(0, runner), `[and ${cut} frames inside ${RUNNER_DIRECTORY}]`]
36085
36094
  };
36086
36095
  }
36096
+ function elsewhereIn(projectPath, asked) {
36097
+ const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
36098
+ if (wanted === "" || existsSync12(join15(projectPath, wanted))) {
36099
+ return "";
36100
+ }
36101
+ const root = (wanted.split("/")[0] ?? "").toLowerCase();
36102
+ let near = [];
36103
+ try {
36104
+ near = readdirSync6(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
36105
+ } catch {
36106
+ return "";
36107
+ }
36108
+ const there = near.map((name) => `res://${name}`).join(" and ");
36109
+ return near.length === 0 ? "" : ` The project does have ${there}.`;
36110
+ }
36087
36111
  function hasMainScene(projectFile) {
36088
36112
  const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
36089
36113
  return typeof scene === "string" && scene !== "";
@@ -36708,6 +36732,15 @@ class GodotServer {
36708
36732
  }
36709
36733
  async debuggedGame() {
36710
36734
  const game = this.activeProcess;
36735
+ if (game !== null && game.exitCode !== null) {
36736
+ return {
36737
+ ok: false,
36738
+ response: this.createErrorResponse(`The last run has already finished (exit code ${game.exitCode}), so there is no debug session to answer for.`, [
36739
+ "editor_output still reads what it printed, until the next run starts",
36740
+ "editor_run start plays another, which the debugger can hold"
36741
+ ])
36742
+ };
36743
+ }
36711
36744
  if (game === null) {
36712
36745
  return {
36713
36746
  ok: false,
@@ -36975,6 +37008,7 @@ class GodotServer {
36975
37008
  }
36976
37009
  const reports = "res://.godot/gdharness-reports";
36977
37010
  const ignored = readStringArray(args, "ignore") ?? [];
37011
+ const asked = readString(contained.value, "path") ?? "res://test";
36978
37012
  const cmdArgs = [
36979
37013
  "--headless",
36980
37014
  "--path",
@@ -36984,7 +37018,7 @@ class GodotServer {
36984
37018
  "--ignoreHeadlessMode",
36985
37019
  ...readBoolean(args, "failFast") === true ? [] : ["-c"],
36986
37020
  "-a",
36987
- readString(contained.value, "path") ?? "res://test",
37021
+ asked,
36988
37022
  ...ignored.flatMap((entry) => ["-i", entry]),
36989
37023
  "-rd",
36990
37024
  reports,
@@ -37034,21 +37068,25 @@ class GodotServer {
37034
37068
  105: "script errors"
37035
37069
  };
37036
37070
  const exitCode = run.exitCode;
37037
- const verdict = hung ? `hung: killed after ${timeoutMs} ms` : verdicts[exitCode ?? -1] ?? `exit ${exitCode ?? "unknown"}`;
37071
+ const printed = run.log.select({ severity: "info", sinceLastCall: false, limit: 200 }).entries;
37072
+ const said = printed.map((entry) => entry.text);
37073
+ const nothingRan = hung ? null : whyNoReport(said, asked);
37074
+ const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? verdicts[exitCode ?? -1] ?? `exit ${exitCode ?? "unknown"}`;
37038
37075
  if (report === null) {
37076
+ const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
37039
37077
  return {
37040
37078
  content: [
37041
- {
37042
- type: "text",
37043
- text: `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).`
37044
- },
37079
+ { type: "text", text: note },
37045
37080
  {
37046
37081
  type: "text",
37047
37082
  text: JSON.stringify({
37083
+ passed: false,
37084
+ verdict,
37085
+ tests: 0,
37048
37086
  exitCode,
37049
37087
  hung,
37050
37088
  arguments: cmdArgs,
37051
- entries: run.log.select({ severity: "info", sinceLastCall: false, limit: 60 }).entries
37089
+ entries: printed.slice(0, 60)
37052
37090
  }, null, 2)
37053
37091
  }
37054
37092
  ],
@@ -37200,7 +37238,7 @@ class GodotServer {
37200
37238
  version: godotPath === null ? null : await this.godotVersion(godotPath)
37201
37239
  },
37202
37240
  game: {
37203
- processActive: this.activeProcess !== null,
37241
+ processActive: this.activeProcess?.exitCode === null,
37204
37242
  playingInEditor: playing,
37205
37243
  runtimeConnected: games.some((game) => game.reachable),
37206
37244
  runtimes: games
@@ -37364,7 +37402,7 @@ class GodotServer {
37364
37402
  if (op === "check") {
37365
37403
  return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
37366
37404
  }
37367
- if (this.activeProcess) {
37405
+ if (this.activeProcess?.exitCode === null) {
37368
37406
  this.logDebug("Ending the running game before starting another");
37369
37407
  await this.endActiveGame();
37370
37408
  }
@@ -37383,11 +37421,6 @@ class GodotServer {
37383
37421
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
37384
37422
  const started = this.spawnGame(engine.value, cmdArgs);
37385
37423
  this.activeProcess = started;
37386
- started.process.on("exit", () => {
37387
- if (this.activeProcess === started) {
37388
- this.activeProcess = null;
37389
- }
37390
- });
37391
37424
  return this.jsonTextResponse({
37392
37425
  started: true,
37393
37426
  through: "gdharness",
@@ -38603,6 +38636,27 @@ function disconnect(harness, projectPath) {
38603
38636
  `, "utf8");
38604
38637
  return { harness, path, action: "removed" };
38605
38638
  }
38639
+ function harnessNote(moved, now) {
38640
+ if (moved.length === 0) {
38641
+ return ` 2. Your harness config already named this version, but the server it is running is
38642
+ ` + ` whichever one it spawned when the session started, which this cannot see from here.
38643
+ ` + " Reconnect the MCP server, or restart the harness.";
38644
+ }
38645
+ return ` 2. Your harness is still running the server it spawned from ${moved.join(", ")}.
38646
+ ` + ` Its config names ${now} now.
38647
+ ` + " Reconnect the MCP server, or restart the harness.";
38648
+ }
38649
+ function launchedBy(entry) {
38650
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
38651
+ return;
38652
+ }
38653
+ const held = entry;
38654
+ const whole = held["command"];
38655
+ const args = Array.isArray(held["args"]) ? held["args"] : [];
38656
+ const parts = Array.isArray(whole) ? whole : [whole, ...args];
38657
+ const said = parts.filter((part) => typeof part === "string");
38658
+ return said.length === 0 ? undefined : said.join(" ");
38659
+ }
38606
38660
  function connect(harness, projectPath, launch) {
38607
38661
  const path = configPath(harness, projectPath);
38608
38662
  if (harness.snippet !== undefined) {
@@ -38627,8 +38681,11 @@ function connect(harness, projectPath, launch) {
38627
38681
  }
38628
38682
  const container = typeof existing === "object" && existing !== null && !Array.isArray(existing) ? existing[harness.container] : undefined;
38629
38683
  const already = typeof container === "object" && container !== null && SERVER_KEY in container;
38630
- return put(harness, path, `${JSON.stringify(merged(existing, harness, launch), null, 2)}
38684
+ const written = put(harness, path, `${JSON.stringify(merged(existing, harness, launch), null, 2)}
38631
38685
  `, already);
38686
+ const was = launchedBy(container?.[SERVER_KEY]);
38687
+ const now = launchedBy(entryFor(harness.shape, launch));
38688
+ return was === undefined || was === now ? written : { ...written, wasLaunchedBy: was };
38632
38689
  }
38633
38690
  function put(harness, path, contents, already) {
38634
38691
  mkdirSync(dirname2(path), { recursive: true });
@@ -39315,11 +39372,16 @@ function reportConnection(group, launch, projectPath) {
39315
39372
  return;
39316
39373
  }
39317
39374
  console.log(`${who}: ${written.action} ${written.path}`);
39375
+ if (written.wasLaunchedBy !== undefined) {
39376
+ console.log(` it was launched by ${written.wasLaunchedBy}`);
39377
+ console.log(` now by ${[launch.command, ...launch.args].join(" ")}`);
39378
+ }
39318
39379
  for (const harness of group.harnesses) {
39319
39380
  if (harness.manual !== undefined) {
39320
39381
  console.log(` ${harness.name}: ${harness.manual}`);
39321
39382
  }
39322
39383
  }
39384
+ return written.wasLaunchedBy;
39323
39385
  }
39324
39386
  function nextSteps(projectPath, runtime) {
39325
39387
  console.log(`
@@ -39453,8 +39515,12 @@ async function upgrade() {
39453
39515
  said2(await runOperation(godot, "refresh_class_cache", {}, projectPath), "rebuilding the class list");
39454
39516
  const launch = launchFor(version, godot.godotPath, projectPath);
39455
39517
  const already = HARNESSES.filter((harness) => registered(harness, projectPath));
39518
+ const moved = [];
39456
39519
  for (const group of groupByFile(already, projectPath)) {
39457
- reportConnection(group, launch, projectPath);
39520
+ const was = reportConnection(group, launch, projectPath);
39521
+ if (was !== undefined && !moved.includes(was)) {
39522
+ moved.push(was);
39523
+ }
39458
39524
  }
39459
39525
  if (already.length === 0) {
39460
39526
  console.log("no harness config names gdharness, so none was re-pinned");
@@ -39466,8 +39532,7 @@ async function upgrade() {
39466
39532
  On ${version}. Two things this could not do for you:`);
39467
39533
  console.log(` 1. The open editor is still running the addons it loaded at startup. Restart it with the
39468
39534
  ` + " editor_launch restart tool, which closes and reopens the window.");
39469
- console.log(` 2. Your harness is still running gdharness ${installed}: its config named that version when
39470
- ` + " the server was spawned. Reconnect the MCP server, or restart the harness.");
39535
+ console.log(harnessNote(moved, [launch.command, ...launch.args].join(" ")));
39471
39536
  }
39472
39537
  function doctorReport(projectPath) {
39473
39538
  const report = inspectProject(projectPath);
package/build/index.js CHANGED
@@ -26583,6 +26583,15 @@ function parseJUnit(xml) {
26583
26583
  suites
26584
26584
  };
26585
26585
  }
26586
+ var NOTHING_RAN = /no test cases found/i;
26587
+ var NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
26588
+ function whyNoReport(printed, asked) {
26589
+ const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
26590
+ if (missing !== undefined) {
26591
+ return `nothing at ${missing[1] ?? asked}`;
26592
+ }
26593
+ return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
26594
+ }
26586
26595
 
26587
26596
  // src/lsp_client.ts
26588
26597
  import { realpathSync } from "node:fs";
@@ -27987,7 +27996,7 @@ var TOOL_SPECS = [
27987
27996
  },
27988
27997
  {
27989
27998
  name: "project_test",
27990
- 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. 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. Needs gdUnit4 under addons/gdUnit4.",
27999
+ 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. 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. 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. Needs gdUnit4 under addons/gdUnit4.",
27991
28000
  parameters: {
27992
28001
  projectPath: PROJECT_PATH,
27993
28002
  path: {
@@ -28262,7 +28271,7 @@ var TOOL_SPECS = [
28262
28271
  },
28263
28272
  {
28264
28273
  name: "editor_run",
28265
- description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until stop, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
28274
+ description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
28266
28275
  parameters: {
28267
28276
  projectPath: PROJECT_PATH,
28268
28277
  scene: { type: "string", description: "A scene to run instead of the main scene." },
@@ -28275,7 +28284,7 @@ var TOOL_SPECS = [
28275
28284
  },
28276
28285
  requires: ["projectPath"],
28277
28286
  operations: {
28278
- start: { summary: "run the project until stop", requires: [] },
28287
+ start: { summary: "run the project until it quits or is stopped", requires: [] },
28279
28288
  stop: { summary: "end the run and answer with what it printed last", requires: [] },
28280
28289
  check: { summary: "boot headless, quit after a few frames, and report the verdict", requires: [] }
28281
28290
  },
@@ -28283,7 +28292,7 @@ var TOOL_SPECS = [
28283
28292
  },
28284
28293
  {
28285
28294
  name: "editor_output",
28286
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries.",
28295
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts.",
28287
28296
  parameters: {
28288
28297
  severity: {
28289
28298
  type: "string",
@@ -28864,6 +28873,21 @@ function aboveTheRunner(entry) {
28864
28873
  detail: [...entry.detail.slice(0, runner), `[and ${cut} frames inside ${RUNNER_DIRECTORY}]`]
28865
28874
  };
28866
28875
  }
28876
+ function elsewhereIn(projectPath, asked) {
28877
+ const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
28878
+ if (wanted === "" || existsSync7(join9(projectPath, wanted))) {
28879
+ return "";
28880
+ }
28881
+ const root = (wanted.split("/")[0] ?? "").toLowerCase();
28882
+ let near = [];
28883
+ try {
28884
+ near = readdirSync5(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
28885
+ } catch {
28886
+ return "";
28887
+ }
28888
+ const there = near.map((name) => `res://${name}`).join(" and ");
28889
+ return near.length === 0 ? "" : ` The project does have ${there}.`;
28890
+ }
28867
28891
  function hasMainScene(projectFile) {
28868
28892
  const scene = parseProjectGodot(readFileSync9(projectFile, "utf8"))["application"]?.["run/main_scene"];
28869
28893
  return typeof scene === "string" && scene !== "";
@@ -29488,6 +29512,15 @@ class GodotServer {
29488
29512
  }
29489
29513
  async debuggedGame() {
29490
29514
  const game = this.activeProcess;
29515
+ if (game !== null && game.exitCode !== null) {
29516
+ return {
29517
+ ok: false,
29518
+ response: this.createErrorResponse(`The last run has already finished (exit code ${game.exitCode}), so there is no debug session to answer for.`, [
29519
+ "editor_output still reads what it printed, until the next run starts",
29520
+ "editor_run start plays another, which the debugger can hold"
29521
+ ])
29522
+ };
29523
+ }
29491
29524
  if (game === null) {
29492
29525
  return {
29493
29526
  ok: false,
@@ -29755,6 +29788,7 @@ class GodotServer {
29755
29788
  }
29756
29789
  const reports = "res://.godot/gdharness-reports";
29757
29790
  const ignored = readStringArray(args, "ignore") ?? [];
29791
+ const asked = readString(contained.value, "path") ?? "res://test";
29758
29792
  const cmdArgs = [
29759
29793
  "--headless",
29760
29794
  "--path",
@@ -29764,7 +29798,7 @@ class GodotServer {
29764
29798
  "--ignoreHeadlessMode",
29765
29799
  ...readBoolean(args, "failFast") === true ? [] : ["-c"],
29766
29800
  "-a",
29767
- readString(contained.value, "path") ?? "res://test",
29801
+ asked,
29768
29802
  ...ignored.flatMap((entry) => ["-i", entry]),
29769
29803
  "-rd",
29770
29804
  reports,
@@ -29814,21 +29848,25 @@ class GodotServer {
29814
29848
  105: "script errors"
29815
29849
  };
29816
29850
  const exitCode = run.exitCode;
29817
- const verdict = hung ? `hung: killed after ${timeoutMs} ms` : verdicts[exitCode ?? -1] ?? `exit ${exitCode ?? "unknown"}`;
29851
+ const printed = run.log.select({ severity: "info", sinceLastCall: false, limit: 200 }).entries;
29852
+ const said = printed.map((entry) => entry.text);
29853
+ const nothingRan = hung ? null : whyNoReport(said, asked);
29854
+ const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? verdicts[exitCode ?? -1] ?? `exit ${exitCode ?? "unknown"}`;
29818
29855
  if (report === null) {
29856
+ const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
29819
29857
  return {
29820
29858
  content: [
29821
- {
29822
- type: "text",
29823
- text: `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).`
29824
- },
29859
+ { type: "text", text: note },
29825
29860
  {
29826
29861
  type: "text",
29827
29862
  text: JSON.stringify({
29863
+ passed: false,
29864
+ verdict,
29865
+ tests: 0,
29828
29866
  exitCode,
29829
29867
  hung,
29830
29868
  arguments: cmdArgs,
29831
- entries: run.log.select({ severity: "info", sinceLastCall: false, limit: 60 }).entries
29869
+ entries: printed.slice(0, 60)
29832
29870
  }, null, 2)
29833
29871
  }
29834
29872
  ],
@@ -29980,7 +30018,7 @@ class GodotServer {
29980
30018
  version: godotPath === null ? null : await this.godotVersion(godotPath)
29981
30019
  },
29982
30020
  game: {
29983
- processActive: this.activeProcess !== null,
30021
+ processActive: this.activeProcess?.exitCode === null,
29984
30022
  playingInEditor: playing,
29985
30023
  runtimeConnected: games.some((game) => game.reachable),
29986
30024
  runtimes: games
@@ -30144,7 +30182,7 @@ class GodotServer {
30144
30182
  if (op === "check") {
30145
30183
  return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
30146
30184
  }
30147
- if (this.activeProcess) {
30185
+ if (this.activeProcess?.exitCode === null) {
30148
30186
  this.logDebug("Ending the running game before starting another");
30149
30187
  await this.endActiveGame();
30150
30188
  }
@@ -30163,11 +30201,6 @@ class GodotServer {
30163
30201
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
30164
30202
  const started = this.spawnGame(engine.value, cmdArgs);
30165
30203
  this.activeProcess = started;
30166
- started.process.on("exit", () => {
30167
- if (this.activeProcess === started) {
30168
- this.activeProcess = null;
30169
- }
30170
- });
30171
30204
  return this.jsonTextResponse({
30172
30205
  started: true,
30173
30206
  through: "gdharness",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
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",