gdharness 0.9.4 → 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 +56 -13
- package/build/index.js +38 -9
- package/package.json +1 -1
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 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. 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." },
|
|
@@ -35141,7 +35141,14 @@ function parseJUnit(xml) {
|
|
|
35141
35141
|
suites
|
|
35142
35142
|
};
|
|
35143
35143
|
}
|
|
35144
|
-
|
|
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 !== "";
|
|
@@ -36984,6 +37008,7 @@ class GodotServer {
|
|
|
36984
37008
|
}
|
|
36985
37009
|
const reports = "res://.godot/gdharness-reports";
|
|
36986
37010
|
const ignored = readStringArray(args, "ignore") ?? [];
|
|
37011
|
+
const asked = readString(contained.value, "path") ?? "res://test";
|
|
36987
37012
|
const cmdArgs = [
|
|
36988
37013
|
"--headless",
|
|
36989
37014
|
"--path",
|
|
@@ -36993,7 +37018,7 @@ class GodotServer {
|
|
|
36993
37018
|
"--ignoreHeadlessMode",
|
|
36994
37019
|
...readBoolean(args, "failFast") === true ? [] : ["-c"],
|
|
36995
37020
|
"-a",
|
|
36996
|
-
|
|
37021
|
+
asked,
|
|
36997
37022
|
...ignored.flatMap((entry) => ["-i", entry]),
|
|
36998
37023
|
"-rd",
|
|
36999
37024
|
reports,
|
|
@@ -37043,21 +37068,25 @@ class GodotServer {
|
|
|
37043
37068
|
105: "script errors"
|
|
37044
37069
|
};
|
|
37045
37070
|
const exitCode = run.exitCode;
|
|
37046
|
-
const
|
|
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"}`;
|
|
37047
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)}`;
|
|
37048
37077
|
return {
|
|
37049
37078
|
content: [
|
|
37050
|
-
{
|
|
37051
|
-
type: "text",
|
|
37052
|
-
text: `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).`
|
|
37053
|
-
},
|
|
37079
|
+
{ type: "text", text: note },
|
|
37054
37080
|
{
|
|
37055
37081
|
type: "text",
|
|
37056
37082
|
text: JSON.stringify({
|
|
37083
|
+
passed: false,
|
|
37084
|
+
verdict,
|
|
37085
|
+
tests: 0,
|
|
37057
37086
|
exitCode,
|
|
37058
37087
|
hung,
|
|
37059
37088
|
arguments: cmdArgs,
|
|
37060
|
-
entries:
|
|
37089
|
+
entries: printed.slice(0, 60)
|
|
37061
37090
|
}, null, 2)
|
|
37062
37091
|
}
|
|
37063
37092
|
],
|
|
@@ -38607,6 +38636,16 @@ function disconnect(harness, projectPath) {
|
|
|
38607
38636
|
`, "utf8");
|
|
38608
38637
|
return { harness, path, action: "removed" };
|
|
38609
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
|
+
}
|
|
38610
38649
|
function launchedBy(entry) {
|
|
38611
38650
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
38612
38651
|
return;
|
|
@@ -39342,6 +39381,7 @@ function reportConnection(group, launch, projectPath) {
|
|
|
39342
39381
|
console.log(` ${harness.name}: ${harness.manual}`);
|
|
39343
39382
|
}
|
|
39344
39383
|
}
|
|
39384
|
+
return written.wasLaunchedBy;
|
|
39345
39385
|
}
|
|
39346
39386
|
function nextSteps(projectPath, runtime) {
|
|
39347
39387
|
console.log(`
|
|
@@ -39475,8 +39515,12 @@ async function upgrade() {
|
|
|
39475
39515
|
said2(await runOperation(godot, "refresh_class_cache", {}, projectPath), "rebuilding the class list");
|
|
39476
39516
|
const launch = launchFor(version, godot.godotPath, projectPath);
|
|
39477
39517
|
const already = HARNESSES.filter((harness) => registered(harness, projectPath));
|
|
39518
|
+
const moved = [];
|
|
39478
39519
|
for (const group of groupByFile(already, projectPath)) {
|
|
39479
|
-
reportConnection(group, launch, projectPath);
|
|
39520
|
+
const was = reportConnection(group, launch, projectPath);
|
|
39521
|
+
if (was !== undefined && !moved.includes(was)) {
|
|
39522
|
+
moved.push(was);
|
|
39523
|
+
}
|
|
39480
39524
|
}
|
|
39481
39525
|
if (already.length === 0) {
|
|
39482
39526
|
console.log("no harness config names gdharness, so none was re-pinned");
|
|
@@ -39488,8 +39532,7 @@ async function upgrade() {
|
|
|
39488
39532
|
On ${version}. Two things this could not do for you:`);
|
|
39489
39533
|
console.log(` 1. The open editor is still running the addons it loaded at startup. Restart it with the
|
|
39490
39534
|
` + " editor_launch restart tool, which closes and reopens the window.");
|
|
39491
|
-
console.log(
|
|
39492
|
-
` + " the server was spawned. Reconnect the MCP server, or restart the harness.");
|
|
39535
|
+
console.log(harnessNote(moved, [launch.command, ...launch.args].join(" ")));
|
|
39493
39536
|
}
|
|
39494
39537
|
function doctorReport(projectPath) {
|
|
39495
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 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. 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." },
|
|
@@ -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 !== "";
|
|
@@ -29764,6 +29788,7 @@ class GodotServer {
|
|
|
29764
29788
|
}
|
|
29765
29789
|
const reports = "res://.godot/gdharness-reports";
|
|
29766
29790
|
const ignored = readStringArray(args, "ignore") ?? [];
|
|
29791
|
+
const asked = readString(contained.value, "path") ?? "res://test";
|
|
29767
29792
|
const cmdArgs = [
|
|
29768
29793
|
"--headless",
|
|
29769
29794
|
"--path",
|
|
@@ -29773,7 +29798,7 @@ class GodotServer {
|
|
|
29773
29798
|
"--ignoreHeadlessMode",
|
|
29774
29799
|
...readBoolean(args, "failFast") === true ? [] : ["-c"],
|
|
29775
29800
|
"-a",
|
|
29776
|
-
|
|
29801
|
+
asked,
|
|
29777
29802
|
...ignored.flatMap((entry) => ["-i", entry]),
|
|
29778
29803
|
"-rd",
|
|
29779
29804
|
reports,
|
|
@@ -29823,21 +29848,25 @@ class GodotServer {
|
|
|
29823
29848
|
105: "script errors"
|
|
29824
29849
|
};
|
|
29825
29850
|
const exitCode = run.exitCode;
|
|
29826
|
-
const
|
|
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"}`;
|
|
29827
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)}`;
|
|
29828
29857
|
return {
|
|
29829
29858
|
content: [
|
|
29830
|
-
{
|
|
29831
|
-
type: "text",
|
|
29832
|
-
text: `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).`
|
|
29833
|
-
},
|
|
29859
|
+
{ type: "text", text: note },
|
|
29834
29860
|
{
|
|
29835
29861
|
type: "text",
|
|
29836
29862
|
text: JSON.stringify({
|
|
29863
|
+
passed: false,
|
|
29864
|
+
verdict,
|
|
29865
|
+
tests: 0,
|
|
29837
29866
|
exitCode,
|
|
29838
29867
|
hung,
|
|
29839
29868
|
arguments: cmdArgs,
|
|
29840
|
-
entries:
|
|
29869
|
+
entries: printed.slice(0, 60)
|
|
29841
29870
|
}, null, 2)
|
|
29842
29871
|
}
|
|
29843
29872
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.9.
|
|
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",
|