gdharness 0.10.2 → 0.11.0
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 +401 -84
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +56 -6
- package/build/index.js +391 -80
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7190,8 +7190,12 @@ function announced(line) {
|
|
|
7190
7190
|
|
|
7191
7191
|
class GameLog {
|
|
7192
7192
|
entries = [];
|
|
7193
|
-
decoders = {
|
|
7194
|
-
|
|
7193
|
+
decoders = {
|
|
7194
|
+
stdout: new StringDecoder("utf8"),
|
|
7195
|
+
stderr: new StringDecoder("utf8"),
|
|
7196
|
+
transcript: new StringDecoder("utf8")
|
|
7197
|
+
};
|
|
7198
|
+
partial = { stdout: "", stderr: "", transcript: "" };
|
|
7195
7199
|
lastHeadline = null;
|
|
7196
7200
|
cursor = 0;
|
|
7197
7201
|
append(source, chunk) {
|
|
@@ -7204,7 +7208,7 @@ class GameLog {
|
|
|
7204
7208
|
}
|
|
7205
7209
|
}
|
|
7206
7210
|
finish() {
|
|
7207
|
-
for (const source of
|
|
7211
|
+
for (const source of STREAMS) {
|
|
7208
7212
|
const tail = this.partial[source] + this.decoders[source].end();
|
|
7209
7213
|
this.partial[source] = "";
|
|
7210
7214
|
if (tail !== "") {
|
|
@@ -7267,10 +7271,11 @@ class GameLog {
|
|
|
7267
7271
|
return { entries: matching.slice(omitted), omitted };
|
|
7268
7272
|
}
|
|
7269
7273
|
}
|
|
7270
|
-
var HEADLINE, COLOUR_CODE;
|
|
7274
|
+
var HEADLINE, COLOUR_CODE, STREAMS;
|
|
7271
7275
|
var init_game_log = __esm(() => {
|
|
7272
7276
|
HEADLINE = /^(USER )?(SCRIPT ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
7273
7277
|
COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
7278
|
+
STREAMS = ["stdout", "stderr", "transcript"];
|
|
7274
7279
|
});
|
|
7275
7280
|
|
|
7276
7281
|
// src/update-check.ts
|
|
@@ -15408,7 +15413,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15408
15413
|
},
|
|
15409
15414
|
{
|
|
15410
15415
|
name: "project_test",
|
|
15411
|
-
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.",
|
|
15416
|
+
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. 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.",
|
|
15412
15417
|
parameters: {
|
|
15413
15418
|
projectPath: PROJECT_PATH,
|
|
15414
15419
|
path: {
|
|
@@ -15820,8 +15825,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15820
15825
|
},
|
|
15821
15826
|
includeHidden: {
|
|
15822
15827
|
type: "boolean",
|
|
15823
|
-
ops: ["text"],
|
|
15824
|
-
description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
|
|
15828
|
+
ops: ["text", "find"],
|
|
15829
|
+
description: "text: read hidden nodes as well, for checking that something is not showing. Default false. find: default true, since a find means the node whether or not it is drawn; false answers with what the player can actually see, which is how a panel that keeps a label for every line and hides the ones that do not is read. A node counts as hidden when anything above it is, and how many matches were left out comes back under hidden."
|
|
15825
15830
|
},
|
|
15826
15831
|
metrics: {
|
|
15827
15832
|
type: "array",
|
|
@@ -15838,7 +15843,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15838
15843
|
requires: []
|
|
15839
15844
|
},
|
|
15840
15845
|
find: {
|
|
15841
|
-
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each",
|
|
15846
|
+
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each, hidden ones included unless includeHidden says otherwise",
|
|
15842
15847
|
requires: []
|
|
15843
15848
|
},
|
|
15844
15849
|
rect: {
|
|
@@ -35168,7 +35173,37 @@ function whyNoReport(printed, asked) {
|
|
|
35168
35173
|
}
|
|
35169
35174
|
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
35170
35175
|
}
|
|
35171
|
-
|
|
35176
|
+
function orphansPrinted(printed) {
|
|
35177
|
+
const suites = [];
|
|
35178
|
+
let total = null;
|
|
35179
|
+
let running = null;
|
|
35180
|
+
for (const line of printed) {
|
|
35181
|
+
const started = RUNNING_SUITE.exec(line);
|
|
35182
|
+
if (started?.[1] !== undefined) {
|
|
35183
|
+
running = started[1];
|
|
35184
|
+
continue;
|
|
35185
|
+
}
|
|
35186
|
+
const counted = ORPHANS.exec(line);
|
|
35187
|
+
if (counted?.[1] === undefined) {
|
|
35188
|
+
continue;
|
|
35189
|
+
}
|
|
35190
|
+
const orphans = Number(counted[1]);
|
|
35191
|
+
if (line.includes(OVERALL)) {
|
|
35192
|
+
total = orphans;
|
|
35193
|
+
running = null;
|
|
35194
|
+
continue;
|
|
35195
|
+
}
|
|
35196
|
+
if (running !== null && orphans > 0) {
|
|
35197
|
+
suites.push({ path: running, orphans });
|
|
35198
|
+
}
|
|
35199
|
+
running = null;
|
|
35200
|
+
}
|
|
35201
|
+
return {
|
|
35202
|
+
total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
|
|
35203
|
+
suites
|
|
35204
|
+
};
|
|
35205
|
+
}
|
|
35206
|
+
var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
|
|
35172
35207
|
var init_junit = __esm(() => {
|
|
35173
35208
|
MalformedReportError = class MalformedReportError extends Error {
|
|
35174
35209
|
constructor(message) {
|
|
@@ -35180,6 +35215,8 @@ var init_junit = __esm(() => {
|
|
|
35180
35215
|
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
35181
35216
|
NOTHING_RAN = /no test cases found/i;
|
|
35182
35217
|
NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
|
|
35218
|
+
RUNNING_SUITE = /Run Test Suite:\s*(\S+)/;
|
|
35219
|
+
ORPHANS = /(\d+)\s+orphans/;
|
|
35183
35220
|
});
|
|
35184
35221
|
|
|
35185
35222
|
// src/lsp_client.ts
|
|
@@ -35483,6 +35520,14 @@ class GodotLSPClient {
|
|
|
35483
35520
|
}
|
|
35484
35521
|
return uri;
|
|
35485
35522
|
}
|
|
35523
|
+
closeDocument(uri) {
|
|
35524
|
+
if (!this.documentVersions.delete(uri)) {
|
|
35525
|
+
return;
|
|
35526
|
+
}
|
|
35527
|
+
try {
|
|
35528
|
+
this.sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
35529
|
+
} catch {}
|
|
35530
|
+
}
|
|
35486
35531
|
async initialize(rootPath) {
|
|
35487
35532
|
await this.ensureConnected();
|
|
35488
35533
|
const resolvedRootPath = resolve3(rootPath);
|
|
@@ -35536,8 +35581,9 @@ class GodotLSPClient {
|
|
|
35536
35581
|
timer
|
|
35537
35582
|
});
|
|
35538
35583
|
});
|
|
35584
|
+
let opened = null;
|
|
35539
35585
|
try {
|
|
35540
|
-
this.syncDocument(filePath, content);
|
|
35586
|
+
opened = this.syncDocument(filePath, content);
|
|
35541
35587
|
return await diagnosticsPromise;
|
|
35542
35588
|
} catch (error) {
|
|
35543
35589
|
const waiter = this.diagnosticsWaiters.get(key);
|
|
@@ -35546,16 +35592,25 @@ class GodotLSPClient {
|
|
|
35546
35592
|
this.diagnosticsWaiters.delete(key);
|
|
35547
35593
|
}
|
|
35548
35594
|
throw error;
|
|
35595
|
+
} finally {
|
|
35596
|
+
if (opened !== null) {
|
|
35597
|
+
this.closeDocument(opened);
|
|
35598
|
+
}
|
|
35549
35599
|
}
|
|
35550
35600
|
}
|
|
35551
35601
|
async getCompletions(filePath, content, line, character) {
|
|
35552
35602
|
await this.ensureConnected();
|
|
35553
35603
|
await this.ensureInitializedForFile(filePath);
|
|
35554
35604
|
const uri = this.syncDocument(filePath, content);
|
|
35555
|
-
|
|
35556
|
-
|
|
35557
|
-
|
|
35558
|
-
|
|
35605
|
+
let result;
|
|
35606
|
+
try {
|
|
35607
|
+
result = await this.sendRequest("textDocument/completion", {
|
|
35608
|
+
textDocument: { uri },
|
|
35609
|
+
position: { line, character }
|
|
35610
|
+
});
|
|
35611
|
+
} finally {
|
|
35612
|
+
this.closeDocument(uri);
|
|
35613
|
+
}
|
|
35559
35614
|
if (Array.isArray(result)) {
|
|
35560
35615
|
return result;
|
|
35561
35616
|
}
|
|
@@ -35571,18 +35626,27 @@ class GodotLSPClient {
|
|
|
35571
35626
|
await this.ensureConnected();
|
|
35572
35627
|
await this.ensureInitializedForFile(filePath);
|
|
35573
35628
|
const uri = this.syncDocument(filePath, content);
|
|
35574
|
-
|
|
35575
|
-
textDocument
|
|
35576
|
-
|
|
35577
|
-
|
|
35629
|
+
try {
|
|
35630
|
+
return await this.sendRequest("textDocument/hover", {
|
|
35631
|
+
textDocument: { uri },
|
|
35632
|
+
position: { line, character }
|
|
35633
|
+
});
|
|
35634
|
+
} finally {
|
|
35635
|
+
this.closeDocument(uri);
|
|
35636
|
+
}
|
|
35578
35637
|
}
|
|
35579
35638
|
async getDocumentSymbols(filePath, content) {
|
|
35580
35639
|
await this.ensureConnected();
|
|
35581
35640
|
await this.ensureInitializedForFile(filePath);
|
|
35582
35641
|
const uri = this.syncDocument(filePath, content);
|
|
35583
|
-
|
|
35584
|
-
|
|
35585
|
-
|
|
35642
|
+
let result;
|
|
35643
|
+
try {
|
|
35644
|
+
result = await this.sendRequest("textDocument/documentSymbol", {
|
|
35645
|
+
textDocument: { uri }
|
|
35646
|
+
});
|
|
35647
|
+
} finally {
|
|
35648
|
+
this.closeDocument(uri);
|
|
35649
|
+
}
|
|
35586
35650
|
return Array.isArray(result) ? result : [];
|
|
35587
35651
|
}
|
|
35588
35652
|
isConnected() {
|
|
@@ -36024,15 +36088,97 @@ var init_runtime_client = __esm(() => {
|
|
|
36024
36088
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
36025
36089
|
});
|
|
36026
36090
|
|
|
36091
|
+
// src/run-record.ts
|
|
36092
|
+
import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync6, readFileSync as readFileSync12, rmSync as rmSync7, statSync as statSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
36093
|
+
import { join as join14 } from "node:path";
|
|
36094
|
+
function runsDirectory() {
|
|
36095
|
+
return join14(runtimeDirectory(), "runs");
|
|
36096
|
+
}
|
|
36097
|
+
function recordPath() {
|
|
36098
|
+
return join14(runsDirectory(), "run.json");
|
|
36099
|
+
}
|
|
36100
|
+
function openTranscript(startedAt) {
|
|
36101
|
+
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36102
|
+
const path = join14(runsDirectory(), `run-${startedAt}.log`);
|
|
36103
|
+
return { path, fd: openSync(path, "a") };
|
|
36104
|
+
}
|
|
36105
|
+
function writeRunRecord(record) {
|
|
36106
|
+
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36107
|
+
writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
36108
|
+
}
|
|
36109
|
+
function readRunRecord() {
|
|
36110
|
+
let parsed;
|
|
36111
|
+
try {
|
|
36112
|
+
parsed = JSON.parse(readFileSync12(recordPath(), "utf8"));
|
|
36113
|
+
} catch {
|
|
36114
|
+
return null;
|
|
36115
|
+
}
|
|
36116
|
+
if (!parsed || typeof parsed !== "object") {
|
|
36117
|
+
return null;
|
|
36118
|
+
}
|
|
36119
|
+
const fields = parsed;
|
|
36120
|
+
const pid = fields["pid"];
|
|
36121
|
+
const transcript = fields["transcript"];
|
|
36122
|
+
const startedAt = fields["startedAt"];
|
|
36123
|
+
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
36124
|
+
return null;
|
|
36125
|
+
}
|
|
36126
|
+
const args = fields["arguments"];
|
|
36127
|
+
return {
|
|
36128
|
+
pid,
|
|
36129
|
+
transcript,
|
|
36130
|
+
startedAt,
|
|
36131
|
+
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
36132
|
+
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : []
|
|
36133
|
+
};
|
|
36134
|
+
}
|
|
36135
|
+
function clearRunRecord() {
|
|
36136
|
+
try {
|
|
36137
|
+
rmSync7(recordPath());
|
|
36138
|
+
} catch {}
|
|
36139
|
+
}
|
|
36140
|
+
function ageMs(path, now) {
|
|
36141
|
+
try {
|
|
36142
|
+
return now - statSync2(path).mtimeMs;
|
|
36143
|
+
} catch {
|
|
36144
|
+
return 0;
|
|
36145
|
+
}
|
|
36146
|
+
}
|
|
36147
|
+
function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
36148
|
+
const directory = runsDirectory();
|
|
36149
|
+
const keep = readRunRecord()?.transcript;
|
|
36150
|
+
let entries;
|
|
36151
|
+
try {
|
|
36152
|
+
entries = readdirSync6(directory);
|
|
36153
|
+
} catch {
|
|
36154
|
+
return;
|
|
36155
|
+
}
|
|
36156
|
+
for (const entry of entries) {
|
|
36157
|
+
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
36158
|
+
continue;
|
|
36159
|
+
}
|
|
36160
|
+
const path = join14(directory, entry);
|
|
36161
|
+
if (path === keep || ageMs(path, now) < keepMs) {
|
|
36162
|
+
continue;
|
|
36163
|
+
}
|
|
36164
|
+
try {
|
|
36165
|
+
rmSync7(path);
|
|
36166
|
+
} catch {}
|
|
36167
|
+
}
|
|
36168
|
+
}
|
|
36169
|
+
var init_run_record = __esm(() => {
|
|
36170
|
+
init_runtime_client();
|
|
36171
|
+
});
|
|
36172
|
+
|
|
36027
36173
|
// src/setup.ts
|
|
36028
|
-
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as
|
|
36029
|
-
import { dirname as dirname8, join as
|
|
36174
|
+
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
|
|
36175
|
+
import { dirname as dirname8, join as join15 } from "node:path";
|
|
36030
36176
|
function installedAddonVersion(projectPath) {
|
|
36031
|
-
const marker =
|
|
36177
|
+
const marker = join15(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
|
|
36032
36178
|
if (!existsSync11(marker)) {
|
|
36033
36179
|
return null;
|
|
36034
36180
|
}
|
|
36035
|
-
const said =
|
|
36181
|
+
const said = readFileSync13(marker, "utf8").trim();
|
|
36036
36182
|
return said === "" ? null : said;
|
|
36037
36183
|
}
|
|
36038
36184
|
var ADDONS2, RUNTIME_AUTOLOAD2, VERSION_MARKER2 = ".gdharness-version";
|
|
@@ -36050,24 +36196,45 @@ var init_setup = __esm(() => {
|
|
|
36050
36196
|
|
|
36051
36197
|
// src/server.ts
|
|
36052
36198
|
import { execFile as execFile5, spawn } from "node:child_process";
|
|
36053
|
-
import {
|
|
36199
|
+
import {
|
|
36200
|
+
closeSync,
|
|
36201
|
+
existsSync as existsSync12,
|
|
36202
|
+
fstatSync,
|
|
36203
|
+
mkdirSync as mkdirSync8,
|
|
36204
|
+
mkdtempSync as mkdtempSync3,
|
|
36205
|
+
openSync as openSync2,
|
|
36206
|
+
readdirSync as readdirSync7,
|
|
36207
|
+
readFileSync as readFileSync14,
|
|
36208
|
+
readSync,
|
|
36209
|
+
realpathSync as realpathSync2,
|
|
36210
|
+
rmSync as rmSync9
|
|
36211
|
+
} from "node:fs";
|
|
36054
36212
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36055
|
-
import { basename as basename2, dirname as dirname9, join as
|
|
36213
|
+
import { basename as basename2, dirname as dirname9, join as join16, normalize as normalize3 } from "node:path";
|
|
36056
36214
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
36057
36215
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
36058
36216
|
import { promisify as promisify5 } from "node:util";
|
|
36059
36217
|
function patienceForFrames(frames, atLeast) {
|
|
36060
36218
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
36061
36219
|
}
|
|
36220
|
+
function stillRunning(run) {
|
|
36221
|
+
if (run?.exitCode !== null || run.endedUnwatched === true) {
|
|
36222
|
+
return false;
|
|
36223
|
+
}
|
|
36224
|
+
if (!run.throughEditor && run.process === null && run.pid !== null) {
|
|
36225
|
+
return alive(run.pid);
|
|
36226
|
+
}
|
|
36227
|
+
return true;
|
|
36228
|
+
}
|
|
36062
36229
|
function alive(pid) {
|
|
36063
|
-
if (pid === undefined) {
|
|
36230
|
+
if (pid === undefined || pid === null) {
|
|
36064
36231
|
return false;
|
|
36065
36232
|
}
|
|
36066
36233
|
try {
|
|
36067
36234
|
process.kill(pid, 0);
|
|
36068
36235
|
return true;
|
|
36069
|
-
} catch {
|
|
36070
|
-
return
|
|
36236
|
+
} catch (error) {
|
|
36237
|
+
return error.code === "EPERM";
|
|
36071
36238
|
}
|
|
36072
36239
|
}
|
|
36073
36240
|
function isOfType(value, kind) {
|
|
@@ -36147,13 +36314,13 @@ function aboveTheRunner(entry) {
|
|
|
36147
36314
|
}
|
|
36148
36315
|
function elsewhereIn(projectPath, asked) {
|
|
36149
36316
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
36150
|
-
if (wanted === "" || existsSync12(
|
|
36317
|
+
if (wanted === "" || existsSync12(join16(projectPath, wanted))) {
|
|
36151
36318
|
return "";
|
|
36152
36319
|
}
|
|
36153
36320
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
36154
36321
|
let near = [];
|
|
36155
36322
|
try {
|
|
36156
|
-
near =
|
|
36323
|
+
near = readdirSync7(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()));
|
|
36157
36324
|
} catch {
|
|
36158
36325
|
return "";
|
|
36159
36326
|
}
|
|
@@ -36161,11 +36328,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
36161
36328
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
36162
36329
|
}
|
|
36163
36330
|
function hasMainScene(projectFile) {
|
|
36164
|
-
const scene = parseProjectGodot(
|
|
36331
|
+
const scene = parseProjectGodot(readFileSync14(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
36165
36332
|
return typeof scene === "string" && scene !== "";
|
|
36166
36333
|
}
|
|
36167
36334
|
function editorPlaysHeadless(projectFile) {
|
|
36168
|
-
const runArgs = parseProjectGodot(
|
|
36335
|
+
const runArgs = parseProjectGodot(readFileSync14(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
36169
36336
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
36170
36337
|
}
|
|
36171
36338
|
function camelCased(params) {
|
|
@@ -36181,7 +36348,7 @@ function camelCased(params) {
|
|
|
36181
36348
|
class GodotServer {
|
|
36182
36349
|
mcp;
|
|
36183
36350
|
locator = new GodotLocator2;
|
|
36184
|
-
operationsScript =
|
|
36351
|
+
operationsScript = join16(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
36185
36352
|
godotBridge;
|
|
36186
36353
|
tools = buildToolDefinitions();
|
|
36187
36354
|
activeProcess = null;
|
|
@@ -36338,10 +36505,7 @@ class GodotServer {
|
|
|
36338
36505
|
this.stopWatchingForASuccessor();
|
|
36339
36506
|
withdrawBridge(this.announcedAt);
|
|
36340
36507
|
this.announcedAt = null;
|
|
36341
|
-
|
|
36342
|
-
this.activeProcess.process?.kill();
|
|
36343
|
-
this.activeProcess = null;
|
|
36344
|
-
}
|
|
36508
|
+
this.activeProcess = null;
|
|
36345
36509
|
if (this.lspClient) {
|
|
36346
36510
|
try {
|
|
36347
36511
|
await this.lspClient.disconnect();
|
|
@@ -36406,14 +36570,7 @@ class GodotServer {
|
|
|
36406
36570
|
return;
|
|
36407
36571
|
}
|
|
36408
36572
|
this.shutdownInitiated = true;
|
|
36409
|
-
|
|
36410
|
-
try {
|
|
36411
|
-
this.activeProcess.process?.kill();
|
|
36412
|
-
} catch (error) {
|
|
36413
|
-
console.error("[SERVER] Failed to kill the Godot process on exit:", errorMessage(error));
|
|
36414
|
-
}
|
|
36415
|
-
this.activeProcess = null;
|
|
36416
|
-
}
|
|
36573
|
+
this.activeProcess = null;
|
|
36417
36574
|
}
|
|
36418
36575
|
setupToolHandlers() {
|
|
36419
36576
|
this.mcp.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: this.tools }));
|
|
@@ -36773,7 +36930,7 @@ class GodotServer {
|
|
|
36773
36930
|
if (path === undefined) {
|
|
36774
36931
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
36775
36932
|
}
|
|
36776
|
-
const file =
|
|
36933
|
+
const file = join16(path, "project.godot");
|
|
36777
36934
|
if (!existsSync12(file)) {
|
|
36778
36935
|
return {
|
|
36779
36936
|
ok: false,
|
|
@@ -36943,7 +37100,7 @@ class GodotServer {
|
|
|
36943
37100
|
if (!engine.ok) {
|
|
36944
37101
|
return engine.response;
|
|
36945
37102
|
}
|
|
36946
|
-
const application = parseProjectGodot(
|
|
37103
|
+
const application = parseProjectGodot(readFileSync14(project.value.file, "utf8"))["application"] ?? {};
|
|
36947
37104
|
const name = application["config/name"];
|
|
36948
37105
|
const mainScene = application["run/main_scene"];
|
|
36949
37106
|
const info = {
|
|
@@ -37006,7 +37163,7 @@ class GodotServer {
|
|
|
37006
37163
|
}
|
|
37007
37164
|
const folder = dirname9(output.absolutePath);
|
|
37008
37165
|
try {
|
|
37009
|
-
|
|
37166
|
+
mkdirSync8(folder, { recursive: true });
|
|
37010
37167
|
} catch (error) {
|
|
37011
37168
|
return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
37012
37169
|
}
|
|
@@ -37068,7 +37225,7 @@ class GodotServer {
|
|
|
37068
37225
|
return contained.response;
|
|
37069
37226
|
}
|
|
37070
37227
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
37071
|
-
if (!existsSync12(
|
|
37228
|
+
if (!existsSync12(join16(project.value.path, runner))) {
|
|
37072
37229
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
37073
37230
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
37074
37231
|
]);
|
|
@@ -37102,7 +37259,7 @@ class GodotServer {
|
|
|
37102
37259
|
];
|
|
37103
37260
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
37104
37261
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
37105
|
-
const userData = mkdtempSync3(
|
|
37262
|
+
const userData = mkdtempSync3(join16(tmpdir5(), "gdharness-tests-"));
|
|
37106
37263
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
37107
37264
|
const hung = await new Promise((resolve) => {
|
|
37108
37265
|
const timer = setTimeout(() => {
|
|
@@ -37118,26 +37275,26 @@ class GodotServer {
|
|
|
37118
37275
|
resolve(false);
|
|
37119
37276
|
});
|
|
37120
37277
|
});
|
|
37121
|
-
const reportsDir =
|
|
37278
|
+
const reportsDir = join16(project.value.path, ".godot", "gdharness-reports");
|
|
37122
37279
|
let report = null;
|
|
37123
37280
|
let reportProblem = null;
|
|
37124
37281
|
try {
|
|
37125
|
-
const written = existsSync12(reportsDir) ?
|
|
37282
|
+
const written = existsSync12(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
37126
37283
|
const newest = written.at(-1);
|
|
37127
37284
|
if (newest !== undefined) {
|
|
37128
|
-
report = parseJUnit(
|
|
37285
|
+
report = parseJUnit(readFileSync14(join16(reportsDir, newest, "results.xml"), "utf8"));
|
|
37129
37286
|
}
|
|
37130
37287
|
} catch (error) {
|
|
37131
37288
|
reportProblem = errorMessage(error);
|
|
37132
37289
|
} finally {
|
|
37133
|
-
|
|
37134
|
-
|
|
37290
|
+
rmSync9(reportsDir, { recursive: true, force: true });
|
|
37291
|
+
rmSync9(userData, { recursive: true, force: true });
|
|
37135
37292
|
}
|
|
37136
37293
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
37137
37294
|
const verdicts = {
|
|
37138
37295
|
0: "passed",
|
|
37139
37296
|
100: "failures",
|
|
37140
|
-
101: "warnings",
|
|
37297
|
+
101: "warnings: orphan nodes",
|
|
37141
37298
|
103: "headless not supported by this gdUnit4",
|
|
37142
37299
|
104: "Godot version not supported by this gdUnit4",
|
|
37143
37300
|
105: "script errors"
|
|
@@ -37169,7 +37326,17 @@ class GodotServer {
|
|
|
37169
37326
|
};
|
|
37170
37327
|
}
|
|
37171
37328
|
const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
|
|
37172
|
-
const
|
|
37329
|
+
const orphans = orphansPrinted(said);
|
|
37330
|
+
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
37331
|
+
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
37332
|
+
const warnings = orphans.suites.map((suite) => ({
|
|
37333
|
+
kind: "orphans",
|
|
37334
|
+
path: suite.path,
|
|
37335
|
+
name: report.suites.find((entry) => entry.path === suite.path)?.name ?? suite.path,
|
|
37336
|
+
orphans: suite.orphans,
|
|
37337
|
+
message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
|
|
37338
|
+
}));
|
|
37339
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
37173
37340
|
return this.jsonTextResponse({
|
|
37174
37341
|
passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
37175
37342
|
verdict,
|
|
@@ -37180,6 +37347,9 @@ class GodotServer {
|
|
|
37180
37347
|
skipped: report.skipped,
|
|
37181
37348
|
time: report.time,
|
|
37182
37349
|
failed,
|
|
37350
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
37351
|
+
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
37352
|
+
note: verdict.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." : undefined,
|
|
37183
37353
|
suites: unclean.map((suite) => ({
|
|
37184
37354
|
name: suite.name,
|
|
37185
37355
|
path: suite.path,
|
|
@@ -37187,6 +37357,7 @@ class GodotServer {
|
|
|
37187
37357
|
failures: suite.failures,
|
|
37188
37358
|
errors: suite.errors,
|
|
37189
37359
|
skipped: suite.skipped,
|
|
37360
|
+
orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
|
|
37190
37361
|
time: suite.time
|
|
37191
37362
|
})),
|
|
37192
37363
|
suitesPassed: report.suites.length - unclean.length,
|
|
@@ -37313,7 +37484,7 @@ class GodotServer {
|
|
|
37313
37484
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
37314
37485
|
},
|
|
37315
37486
|
game: {
|
|
37316
|
-
processActive: this.
|
|
37487
|
+
processActive: stillRunning(this.currentRun()),
|
|
37317
37488
|
playingInEditor: playing,
|
|
37318
37489
|
runtimeConnected: games.some((game) => game.reachable),
|
|
37319
37490
|
runtimes: games
|
|
@@ -37481,7 +37652,7 @@ class GodotServer {
|
|
|
37481
37652
|
if (op === "check") {
|
|
37482
37653
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
37483
37654
|
}
|
|
37484
|
-
if (this.
|
|
37655
|
+
if (stillRunning(this.currentRun())) {
|
|
37485
37656
|
this.logDebug("Ending the running game before starting another");
|
|
37486
37657
|
await this.endActiveGame();
|
|
37487
37658
|
}
|
|
@@ -37501,7 +37672,7 @@ class GodotServer {
|
|
|
37501
37672
|
userArgs: given.value
|
|
37502
37673
|
});
|
|
37503
37674
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
37504
|
-
const started = this.
|
|
37675
|
+
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
37505
37676
|
this.activeProcess = started;
|
|
37506
37677
|
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
|
|
37507
37678
|
return this.jsonTextResponse({
|
|
@@ -37515,7 +37686,7 @@ class GodotServer {
|
|
|
37515
37686
|
});
|
|
37516
37687
|
}
|
|
37517
37688
|
async runtimeUp(projectPath, before) {
|
|
37518
|
-
if (!existsSync12(
|
|
37689
|
+
if (!existsSync12(join16(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
37519
37690
|
return {
|
|
37520
37691
|
listening: false,
|
|
37521
37692
|
note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
|
|
@@ -37550,7 +37721,11 @@ class GodotServer {
|
|
|
37550
37721
|
}
|
|
37551
37722
|
const played = {
|
|
37552
37723
|
process: null,
|
|
37724
|
+
pid: null,
|
|
37553
37725
|
log,
|
|
37726
|
+
transcript: null,
|
|
37727
|
+
readOffset: 0,
|
|
37728
|
+
projectPath,
|
|
37554
37729
|
startedAt: Date.now(),
|
|
37555
37730
|
exitCode: null,
|
|
37556
37731
|
throughEditor: true,
|
|
@@ -37589,6 +37764,7 @@ class GodotServer {
|
|
|
37589
37764
|
async endActiveGame() {
|
|
37590
37765
|
const running = this.activeProcess;
|
|
37591
37766
|
this.activeProcess = null;
|
|
37767
|
+
clearRunRecord();
|
|
37592
37768
|
if (!running) {
|
|
37593
37769
|
return;
|
|
37594
37770
|
}
|
|
@@ -37596,14 +37772,26 @@ class GodotServer {
|
|
|
37596
37772
|
await this.handleViaBridge("stop_playing", {});
|
|
37597
37773
|
return;
|
|
37598
37774
|
}
|
|
37599
|
-
running.process
|
|
37775
|
+
if (running.process !== null) {
|
|
37776
|
+
running.process.kill();
|
|
37777
|
+
return;
|
|
37778
|
+
}
|
|
37779
|
+
if (running.pid !== null) {
|
|
37780
|
+
try {
|
|
37781
|
+
process.kill(running.pid);
|
|
37782
|
+
} catch {}
|
|
37783
|
+
}
|
|
37600
37784
|
}
|
|
37601
37785
|
spawnGame(godotPath, cmdArgs, env) {
|
|
37602
37786
|
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
37603
37787
|
const log = new GameLog;
|
|
37604
37788
|
const started = {
|
|
37605
37789
|
process: child,
|
|
37790
|
+
pid: child.pid ?? null,
|
|
37606
37791
|
log,
|
|
37792
|
+
transcript: null,
|
|
37793
|
+
readOffset: 0,
|
|
37794
|
+
projectPath: null,
|
|
37607
37795
|
startedAt: Date.now(),
|
|
37608
37796
|
exitCode: null,
|
|
37609
37797
|
throughEditor: false,
|
|
@@ -37628,6 +37816,117 @@ class GodotServer {
|
|
|
37628
37816
|
});
|
|
37629
37817
|
return started;
|
|
37630
37818
|
}
|
|
37819
|
+
spawnKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
37820
|
+
const startedAt = Date.now();
|
|
37821
|
+
const transcript = openTranscript(startedAt);
|
|
37822
|
+
let child;
|
|
37823
|
+
try {
|
|
37824
|
+
child = spawn(godotPath, cmdArgs, {
|
|
37825
|
+
stdio: ["ignore", transcript.fd, transcript.fd],
|
|
37826
|
+
detached: true,
|
|
37827
|
+
...env ? { env } : {}
|
|
37828
|
+
});
|
|
37829
|
+
} finally {
|
|
37830
|
+
closeSync(transcript.fd);
|
|
37831
|
+
}
|
|
37832
|
+
child.unref();
|
|
37833
|
+
const log = new GameLog;
|
|
37834
|
+
const started = {
|
|
37835
|
+
process: child,
|
|
37836
|
+
pid: child.pid ?? null,
|
|
37837
|
+
log,
|
|
37838
|
+
transcript: transcript.path,
|
|
37839
|
+
readOffset: 0,
|
|
37840
|
+
projectPath,
|
|
37841
|
+
startedAt,
|
|
37842
|
+
exitCode: null,
|
|
37843
|
+
throughEditor: false,
|
|
37844
|
+
brokeOn: null
|
|
37845
|
+
};
|
|
37846
|
+
child.on("exit", (code) => {
|
|
37847
|
+
this.logDebug(`Godot process exited with code ${code ?? "none"}`);
|
|
37848
|
+
this.drainTranscript(started);
|
|
37849
|
+
log.finish();
|
|
37850
|
+
started.exitCode = code ?? -1;
|
|
37851
|
+
});
|
|
37852
|
+
child.on("error", (err) => {
|
|
37853
|
+
console.error("Failed to start Godot process:", err);
|
|
37854
|
+
log.append("stderr", `${err.message}
|
|
37855
|
+
`);
|
|
37856
|
+
started.exitCode = -1;
|
|
37857
|
+
});
|
|
37858
|
+
if (started.pid !== null) {
|
|
37859
|
+
writeRunRecord({
|
|
37860
|
+
pid: started.pid,
|
|
37861
|
+
transcript: transcript.path,
|
|
37862
|
+
startedAt,
|
|
37863
|
+
projectPath,
|
|
37864
|
+
arguments: cmdArgs
|
|
37865
|
+
});
|
|
37866
|
+
}
|
|
37867
|
+
sweepTranscripts();
|
|
37868
|
+
return started;
|
|
37869
|
+
}
|
|
37870
|
+
drainTranscript(game) {
|
|
37871
|
+
if (game.transcript === null) {
|
|
37872
|
+
return;
|
|
37873
|
+
}
|
|
37874
|
+
let handle;
|
|
37875
|
+
try {
|
|
37876
|
+
handle = openSync2(game.transcript, "r");
|
|
37877
|
+
} catch {
|
|
37878
|
+
return;
|
|
37879
|
+
}
|
|
37880
|
+
try {
|
|
37881
|
+
const size = fstatSync(handle).size;
|
|
37882
|
+
if (size <= game.readOffset) {
|
|
37883
|
+
return;
|
|
37884
|
+
}
|
|
37885
|
+
const buffer = Buffer.alloc(size - game.readOffset);
|
|
37886
|
+
const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
|
|
37887
|
+
game.readOffset += read;
|
|
37888
|
+
game.log.append("transcript", buffer.subarray(0, read));
|
|
37889
|
+
} finally {
|
|
37890
|
+
closeSync(handle);
|
|
37891
|
+
}
|
|
37892
|
+
}
|
|
37893
|
+
adoptRecordedRun() {
|
|
37894
|
+
const record = readRunRecord();
|
|
37895
|
+
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
37896
|
+
return null;
|
|
37897
|
+
}
|
|
37898
|
+
const adopted = {
|
|
37899
|
+
process: null,
|
|
37900
|
+
pid: record.pid,
|
|
37901
|
+
log: new GameLog,
|
|
37902
|
+
transcript: record.transcript,
|
|
37903
|
+
readOffset: 0,
|
|
37904
|
+
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
37905
|
+
startedAt: record.startedAt,
|
|
37906
|
+
exitCode: null,
|
|
37907
|
+
throughEditor: false,
|
|
37908
|
+
brokeOn: null
|
|
37909
|
+
};
|
|
37910
|
+
this.drainTranscript(adopted);
|
|
37911
|
+
if (!alive(record.pid)) {
|
|
37912
|
+
adopted.log.finish();
|
|
37913
|
+
adopted.exitCode = null;
|
|
37914
|
+
adopted.endedUnwatched = true;
|
|
37915
|
+
}
|
|
37916
|
+
this.activeProcess = adopted;
|
|
37917
|
+
return adopted;
|
|
37918
|
+
}
|
|
37919
|
+
couldBeOurs(project) {
|
|
37920
|
+
if (project === "") {
|
|
37921
|
+
return true;
|
|
37922
|
+
}
|
|
37923
|
+
const status = this.godotBridge.getStatus();
|
|
37924
|
+
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
37925
|
+
return mine === null || samePath(mine, project);
|
|
37926
|
+
}
|
|
37927
|
+
currentRun() {
|
|
37928
|
+
return this.activeProcess ?? this.adoptRecordedRun();
|
|
37929
|
+
}
|
|
37631
37930
|
async checkBoot(godotPath, projectPath, scene, args, userArgs) {
|
|
37632
37931
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
37633
37932
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
@@ -37661,37 +37960,47 @@ class GodotServer {
|
|
|
37661
37960
|
});
|
|
37662
37961
|
}
|
|
37663
37962
|
handleGetDebugOutput(args) {
|
|
37664
|
-
|
|
37963
|
+
const run = this.currentRun();
|
|
37964
|
+
if (!run) {
|
|
37665
37965
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
37666
37966
|
}
|
|
37667
|
-
this.drainEditorOutput(
|
|
37967
|
+
this.drainEditorOutput(run);
|
|
37968
|
+
this.drainTranscript(run);
|
|
37969
|
+
if (run.endedUnwatched !== true && !stillRunning(run)) {
|
|
37970
|
+
run.log.finish();
|
|
37971
|
+
run.endedUnwatched = run.exitCode === null;
|
|
37972
|
+
}
|
|
37668
37973
|
const severity = readString(args, "severity");
|
|
37669
|
-
const selected =
|
|
37974
|
+
const selected = run.log.select({
|
|
37670
37975
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
37671
37976
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
37672
37977
|
contains: readNonEmptyString(args, "contains"),
|
|
37673
37978
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
37674
37979
|
});
|
|
37675
|
-
const halt =
|
|
37980
|
+
const halt = run.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
37676
37981
|
return this.jsonTextResponse({
|
|
37677
|
-
running:
|
|
37678
|
-
exitCode:
|
|
37679
|
-
through:
|
|
37680
|
-
pid:
|
|
37681
|
-
errors:
|
|
37682
|
-
warnings:
|
|
37683
|
-
clean:
|
|
37982
|
+
running: stillRunning(run),
|
|
37983
|
+
exitCode: run.exitCode,
|
|
37984
|
+
through: run.throughEditor ? "editor" : "gdharness",
|
|
37985
|
+
pid: run.pid,
|
|
37986
|
+
errors: run.log.count("error"),
|
|
37987
|
+
warnings: run.log.count("warning"),
|
|
37988
|
+
clean: run.log.count("error") === 0,
|
|
37684
37989
|
heldAt: halt,
|
|
37990
|
+
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
37991
|
+
project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
|
|
37992
|
+
note: run.endedUnwatched === true ? "This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript." : undefined,
|
|
37685
37993
|
omitted: selected.omitted,
|
|
37686
37994
|
entries: selected.entries
|
|
37687
37995
|
});
|
|
37688
37996
|
}
|
|
37689
37997
|
async handleStopProject() {
|
|
37690
|
-
|
|
37998
|
+
const stopped = this.currentRun();
|
|
37999
|
+
if (!stopped) {
|
|
37691
38000
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
37692
38001
|
}
|
|
37693
|
-
const stopped = this.activeProcess;
|
|
37694
38002
|
this.drainEditorOutput(stopped);
|
|
38003
|
+
this.drainTranscript(stopped);
|
|
37695
38004
|
this.logDebug("Stopping the running game");
|
|
37696
38005
|
await this.endActiveGame();
|
|
37697
38006
|
return this.jsonTextResponse({
|
|
@@ -37797,8 +38106,8 @@ class GodotServer {
|
|
|
37797
38106
|
return this.createErrorResponse(choice.problem);
|
|
37798
38107
|
}
|
|
37799
38108
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
37800
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
37801
|
-
const screenshotPath = screenshotDir ?
|
|
38109
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join16(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
38110
|
+
const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
|
|
37802
38111
|
try {
|
|
37803
38112
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
37804
38113
|
if (!reply.ok) {
|
|
@@ -37819,12 +38128,12 @@ class GodotServer {
|
|
|
37819
38128
|
return {
|
|
37820
38129
|
content: [
|
|
37821
38130
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
37822
|
-
{ type: "image", data:
|
|
38131
|
+
{ type: "image", data: readFileSync14(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
37823
38132
|
]
|
|
37824
38133
|
};
|
|
37825
38134
|
} finally {
|
|
37826
38135
|
if (screenshotDir) {
|
|
37827
|
-
|
|
38136
|
+
rmSync9(screenshotDir, { recursive: true, force: true });
|
|
37828
38137
|
}
|
|
37829
38138
|
}
|
|
37830
38139
|
}
|
|
@@ -37849,12 +38158,14 @@ class GodotServer {
|
|
|
37849
38158
|
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
|
|
37850
38159
|
}
|
|
37851
38160
|
const property = readNonEmptyString(args, "property");
|
|
38161
|
+
const includeHidden = readBoolean(args, "includeHidden");
|
|
37852
38162
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
37853
38163
|
...filters,
|
|
37854
38164
|
projectPath: args["projectPath"],
|
|
37855
38165
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37856
38166
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
37857
|
-
...property === undefined ? {} : { property }
|
|
38167
|
+
...property === undefined ? {} : { property },
|
|
38168
|
+
...includeHidden === undefined ? {} : { include_hidden: includeHidden }
|
|
37858
38169
|
});
|
|
37859
38170
|
}
|
|
37860
38171
|
async handleRuntimeWait(op, args) {
|
|
@@ -37905,6 +38216,7 @@ var init_server3 = __esm(() => {
|
|
|
37905
38216
|
init_ports();
|
|
37906
38217
|
init_project_scan();
|
|
37907
38218
|
init_resources();
|
|
38219
|
+
init_run_record();
|
|
37908
38220
|
init_runtime_client();
|
|
37909
38221
|
init_server_version();
|
|
37910
38222
|
init_setup();
|
|
@@ -37975,7 +38287,7 @@ var init_server3 = __esm(() => {
|
|
|
37975
38287
|
|
|
37976
38288
|
// src/cli.ts
|
|
37977
38289
|
import { existsSync as existsSync13 } from "fs";
|
|
37978
|
-
import { join as
|
|
38290
|
+
import { join as join17, resolve as resolve5 } from "path";
|
|
37979
38291
|
|
|
37980
38292
|
// src/errors.ts
|
|
37981
38293
|
class Refusal extends Error {
|
|
@@ -39366,6 +39678,11 @@ them. \`editor_rescan\` asks it to scan and names any class it still cannot reso
|
|
|
39366
39678
|
declaration and the cache can both be right while the editor stays blind to it. Change the
|
|
39367
39679
|
declaring script, or restart the editor.
|
|
39368
39680
|
|
|
39681
|
+
A diagnostic about a member rather than a type, \`Static function "x()" not found in base "Y"\`
|
|
39682
|
+
about something the engine compiles, is a different thing and is no longer stale here: each ask
|
|
39683
|
+
gives the document back when its answer arrives, so the next one is read off disk. If one turns
|
|
39684
|
+
up anyway, believe the run over the diagnostic and say so.
|
|
39685
|
+
|
|
39369
39686
|
## Refusals are useful
|
|
39370
39687
|
|
|
39371
39688
|
An unknown op, an argument the tool does not name, or a missing required one is refused with the
|
|
@@ -39478,7 +39795,7 @@ class UsageError extends Refusal {
|
|
|
39478
39795
|
function projectArgument(at) {
|
|
39479
39796
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
39480
39797
|
const projectPath = resolve5(named ?? ".");
|
|
39481
|
-
if (!existsSync13(
|
|
39798
|
+
if (!existsSync13(join17(projectPath, "project.godot"))) {
|
|
39482
39799
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
39483
39800
|
}
|
|
39484
39801
|
return projectPath;
|