gdharness 1.1.41 → 1.1.42

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
@@ -17592,8 +17592,9 @@ var init_tool_definitions = __esm(() => {
17592
17592
  parameters: {
17593
17593
  projectPath: PROJECT_PATH,
17594
17594
  path: {
17595
- type: "string",
17596
- description: "A test directory or one suite file inside the project. Default test."
17595
+ type: ["string", "array"],
17596
+ items: { type: "string" },
17597
+ description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. A run where any entry is not there is refused before the engine starts, naming each one. Default test."
17597
17598
  },
17598
17599
  ignore: {
17599
17600
  type: "array",
@@ -42085,6 +42086,36 @@ function elsewhereIn(projectPath, asked) {
42085
42086
  const there = near.map((name) => `res://${name}`).join(" and ");
42086
42087
  return near.length === 0 ? "" : ` The project does have ${there}.`;
42087
42088
  }
42089
+ function testPathsIn(projectPath, given) {
42090
+ const entries = given === undefined || given === null || given === "" ? ["test"] : given;
42091
+ const listed = Array.isArray(entries) ? entries : [entries];
42092
+ if (listed.length === 0) {
42093
+ return {
42094
+ ok: false,
42095
+ reason: "path is an empty list: name a suite file or a directory, or leave path out for test."
42096
+ };
42097
+ }
42098
+ const resolved = [];
42099
+ for (const [index, entry] of listed.entries()) {
42100
+ if (typeof entry !== "string" || entry.trim() === "") {
42101
+ return {
42102
+ ok: false,
42103
+ reason: `path${Array.isArray(entries) ? `[${index}]` : ""} is ${typeof entry === "string" ? "empty" : describeValue(entry)}, not a suite file or a directory in the project.`
42104
+ };
42105
+ }
42106
+ const location = resolveWithinProject(projectPath, entry);
42107
+ if (!location.ok) {
42108
+ return { ok: false, reason: location.reason };
42109
+ }
42110
+ resolved.push(location.relativePath.replace(/\/+$/, ""));
42111
+ }
42112
+ const unique = [...new Set(resolved)];
42113
+ const within = (path, directory) => path.startsWith(`${directory}/`);
42114
+ return {
42115
+ ok: true,
42116
+ value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
42117
+ };
42118
+ }
42088
42119
  function hasMainScene(projectFile) {
42089
42120
  const scene = parseProjectGodot(readFileSync21(projectFile, "utf8"))["application"]?.["run/main_scene"];
42090
42121
  return typeof scene === "string" && scene !== "";
@@ -43600,9 +43631,24 @@ class GodotServer {
43600
43631
  if (!project.ok) {
43601
43632
  return project.response;
43602
43633
  }
43603
- const contained = this.containProjectFiles({ ...args, path: readNonEmptyString(args, "path") ?? "test" });
43604
- if (!contained.ok) {
43605
- return contained.response;
43634
+ const paths = testPathsIn(project.value.path, args["path"]);
43635
+ if (!paths.ok) {
43636
+ return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
43637
+ }
43638
+ const asked = paths.value;
43639
+ const missing = asked.filter((path) => !existsSync19(join24(project.value.path, path.slice("res://".length))));
43640
+ if (missing.length > 0) {
43641
+ const verdict = `nothing at ${missing.join(" and ")}`;
43642
+ return {
43643
+ content: [
43644
+ {
43645
+ type: "text",
43646
+ text: `No tests ran: ${verdict}.${elsewhereIn(project.value.path, missing[0] ?? "")}`
43647
+ },
43648
+ { type: "text", text: answerJson({ passed: false, verdict, tests: 0 }) }
43649
+ ],
43650
+ isError: true
43651
+ };
43606
43652
  }
43607
43653
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
43608
43654
  if (!existsSync19(join24(project.value.path, runner))) {
@@ -43622,7 +43668,6 @@ class GodotServer {
43622
43668
  const ours = `gdharness-reports/${randomUUID2()}`;
43623
43669
  const reports = `res://.godot/${ours}`;
43624
43670
  const ignored = readStringArray(args, "ignore") ?? [];
43625
- const asked = readString(contained.value, "path") ?? "res://test";
43626
43671
  const cmdArgs = [
43627
43672
  "--headless",
43628
43673
  "--path",
@@ -43631,8 +43676,7 @@ class GodotServer {
43631
43676
  `res://${runner}`,
43632
43677
  "--ignoreHeadlessMode",
43633
43678
  ...readBoolean(args, "failFast") === true ? [] : ["-c"],
43634
- "-a",
43635
- asked,
43679
+ ...asked.flatMap((path) => ["-a", path]),
43636
43680
  ...ignored.flatMap((entry) => ["-i", entry]),
43637
43681
  "-rd",
43638
43682
  reports,
@@ -43705,7 +43749,7 @@ class GodotServer {
43705
43749
  limit: Number.POSITIVE_INFINITY
43706
43750
  }).entries;
43707
43751
  const said = printed.map((entry) => entry.text);
43708
- const nothingRan = timedOut ? null : whyNoReport(said, asked);
43752
+ const nothingRan = timedOut ? null : whyNoReport(said, asked.join(" and "));
43709
43753
  const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
43710
43754
  const hung = cutShort?.hung ?? false;
43711
43755
  const scriptErrors = timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
@@ -43719,7 +43763,7 @@ class GodotServer {
43719
43763
  ...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
43720
43764
  };
43721
43765
  if (report === null) {
43722
- const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${verdict}.` : `The test run wrote no report (${verdict}).`;
43766
+ const note = nothingRan !== null ? `No tests ran: ${verdict}.` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${verdict}.` : `The test run wrote no report (${verdict}).`;
43723
43767
  return {
43724
43768
  content: [
43725
43769
  { type: "text", text: note },
package/build/index.js CHANGED
@@ -33013,8 +33013,9 @@ var TOOL_SPECS = [
33013
33013
  parameters: {
33014
33014
  projectPath: PROJECT_PATH,
33015
33015
  path: {
33016
- type: "string",
33017
- description: "A test directory or one suite file inside the project. Default test."
33016
+ type: ["string", "array"],
33017
+ items: { type: "string" },
33018
+ description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. A run where any entry is not there is refused before the engine starts, naming each one. Default test."
33018
33019
  },
33019
33020
  ignore: {
33020
33021
  type: "array",
@@ -34659,6 +34660,36 @@ function elsewhereIn(projectPath, asked) {
34659
34660
  const there = near.map((name) => `res://${name}`).join(" and ");
34660
34661
  return near.length === 0 ? "" : ` The project does have ${there}.`;
34661
34662
  }
34663
+ function testPathsIn(projectPath, given) {
34664
+ const entries = given === undefined || given === null || given === "" ? ["test"] : given;
34665
+ const listed = Array.isArray(entries) ? entries : [entries];
34666
+ if (listed.length === 0) {
34667
+ return {
34668
+ ok: false,
34669
+ reason: "path is an empty list: name a suite file or a directory, or leave path out for test."
34670
+ };
34671
+ }
34672
+ const resolved = [];
34673
+ for (const [index, entry] of listed.entries()) {
34674
+ if (typeof entry !== "string" || entry.trim() === "") {
34675
+ return {
34676
+ ok: false,
34677
+ reason: `path${Array.isArray(entries) ? `[${index}]` : ""} is ${typeof entry === "string" ? "empty" : describeValue(entry)}, not a suite file or a directory in the project.`
34678
+ };
34679
+ }
34680
+ const location = resolveWithinProject(projectPath, entry);
34681
+ if (!location.ok) {
34682
+ return { ok: false, reason: location.reason };
34683
+ }
34684
+ resolved.push(location.relativePath.replace(/\/+$/, ""));
34685
+ }
34686
+ const unique = [...new Set(resolved)];
34687
+ const within = (path, directory) => path.startsWith(`${directory}/`);
34688
+ return {
34689
+ ok: true,
34690
+ value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
34691
+ };
34692
+ }
34662
34693
  function hasMainScene(projectFile) {
34663
34694
  const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
34664
34695
  return typeof scene === "string" && scene !== "";
@@ -36174,9 +36205,24 @@ class GodotServer {
36174
36205
  if (!project.ok) {
36175
36206
  return project.response;
36176
36207
  }
36177
- const contained = this.containProjectFiles({ ...args, path: readNonEmptyString(args, "path") ?? "test" });
36178
- if (!contained.ok) {
36179
- return contained.response;
36208
+ const paths = testPathsIn(project.value.path, args["path"]);
36209
+ if (!paths.ok) {
36210
+ return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
36211
+ }
36212
+ const asked = paths.value;
36213
+ const missing = asked.filter((path) => !existsSync16(join20(project.value.path, path.slice("res://".length))));
36214
+ if (missing.length > 0) {
36215
+ const verdict = `nothing at ${missing.join(" and ")}`;
36216
+ return {
36217
+ content: [
36218
+ {
36219
+ type: "text",
36220
+ text: `No tests ran: ${verdict}.${elsewhereIn(project.value.path, missing[0] ?? "")}`
36221
+ },
36222
+ { type: "text", text: answerJson({ passed: false, verdict, tests: 0 }) }
36223
+ ],
36224
+ isError: true
36225
+ };
36180
36226
  }
36181
36227
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
36182
36228
  if (!existsSync16(join20(project.value.path, runner))) {
@@ -36196,7 +36242,6 @@ class GodotServer {
36196
36242
  const ours = `gdharness-reports/${randomUUID2()}`;
36197
36243
  const reports = `res://.godot/${ours}`;
36198
36244
  const ignored = readStringArray(args, "ignore") ?? [];
36199
- const asked = readString(contained.value, "path") ?? "res://test";
36200
36245
  const cmdArgs = [
36201
36246
  "--headless",
36202
36247
  "--path",
@@ -36205,8 +36250,7 @@ class GodotServer {
36205
36250
  `res://${runner}`,
36206
36251
  "--ignoreHeadlessMode",
36207
36252
  ...readBoolean(args, "failFast") === true ? [] : ["-c"],
36208
- "-a",
36209
- asked,
36253
+ ...asked.flatMap((path) => ["-a", path]),
36210
36254
  ...ignored.flatMap((entry) => ["-i", entry]),
36211
36255
  "-rd",
36212
36256
  reports,
@@ -36279,7 +36323,7 @@ class GodotServer {
36279
36323
  limit: Number.POSITIVE_INFINITY
36280
36324
  }).entries;
36281
36325
  const said = printed.map((entry) => entry.text);
36282
- const nothingRan = timedOut ? null : whyNoReport(said, asked);
36326
+ const nothingRan = timedOut ? null : whyNoReport(said, asked.join(" and "));
36283
36327
  const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
36284
36328
  const hung = cutShort?.hung ?? false;
36285
36329
  const scriptErrors = timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
@@ -36293,7 +36337,7 @@ class GodotServer {
36293
36337
  ...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
36294
36338
  };
36295
36339
  if (report === null) {
36296
- const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${verdict}.` : `The test run wrote no report (${verdict}).`;
36340
+ const note = nothingRan !== null ? `No tests ran: ${verdict}.` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${verdict}.` : `The test run wrote no report (${verdict}).`;
36297
36341
  return {
36298
36342
  content: [
36299
36343
  { type: "text", text: note },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.41",
3
+ "version": "1.1.42",
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",