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 +88 -23
- package/build/index.js +51 -18
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
+
"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",
|