gdharness 1.0.6 → 1.0.7
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 +62 -14
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +28 -5
- package/build/index.js +60 -12
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -15694,7 +15694,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15694
15694
|
requires: []
|
|
15695
15695
|
},
|
|
15696
15696
|
refresh_classes: {
|
|
15697
|
-
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
|
|
15697
|
+
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong. A class the cache held after the last rebuild and had lost when this one began is named under lostSinceLastRebuild: the editor wrote the file without it in between, on a save or a scan, and will again until restarted',
|
|
15698
15698
|
requires: []
|
|
15699
15699
|
}
|
|
15700
15700
|
}
|
|
@@ -15719,7 +15719,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15719
15719
|
},
|
|
15720
15720
|
{
|
|
15721
15721
|
name: "project_test",
|
|
15722
|
-
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. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. 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.",
|
|
15722
|
+
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, and the rebuild's answer is under classes: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. 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. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. 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.",
|
|
15723
15723
|
parameters: {
|
|
15724
15724
|
projectPath: PROJECT_PATH,
|
|
15725
15725
|
path: {
|
|
@@ -15936,7 +15936,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15936
15936
|
},
|
|
15937
15937
|
{
|
|
15938
15938
|
name: "script_diagnostics",
|
|
15939
|
-
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
15939
|
+
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. The one exception is named when it applies: an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
15940
15940
|
parameters: {
|
|
15941
15941
|
projectPath: PROJECT_PATH,
|
|
15942
15942
|
scriptPath: SCRIPT_PATH
|
|
@@ -37616,7 +37616,7 @@ import {
|
|
|
37616
37616
|
statSync as statSync5
|
|
37617
37617
|
} from "node:fs";
|
|
37618
37618
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
37619
|
-
import { basename as basename4, dirname as dirname13, join as join20, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37619
|
+
import { basename as basename4, dirname as dirname13, join as join20, normalize as normalize3, relative as relative3, resolve as resolve5 } from "node:path";
|
|
37620
37620
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
37621
37621
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37622
37622
|
import { promisify as promisify7 } from "node:util";
|
|
@@ -37920,6 +37920,8 @@ class GodotServer {
|
|
|
37920
37920
|
successorWatch = null;
|
|
37921
37921
|
lastProjectPath = null;
|
|
37922
37922
|
shutdownInitiated = false;
|
|
37923
|
+
shortListEditors = new Map;
|
|
37924
|
+
cacheLastRebuilt = new Map;
|
|
37923
37925
|
launchedEditor = null;
|
|
37924
37926
|
launchedFrom = null;
|
|
37925
37927
|
ownProject;
|
|
@@ -38333,8 +38335,15 @@ class GodotServer {
|
|
|
38333
38335
|
const contained = this.containProjectFiles(asks);
|
|
38334
38336
|
return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
|
|
38335
38337
|
}
|
|
38336
|
-
|
|
38337
|
-
|
|
38338
|
+
if (headless !== "refresh_class_cache") {
|
|
38339
|
+
return await this.headless(headless, answerable);
|
|
38340
|
+
}
|
|
38341
|
+
const project = this.project(answerable);
|
|
38342
|
+
if (!project.ok) {
|
|
38343
|
+
return project.response;
|
|
38344
|
+
}
|
|
38345
|
+
const answered = this.answer(await this.rebuildClassCache(project.value.path));
|
|
38346
|
+
return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
38338
38347
|
}
|
|
38339
38348
|
const { op: _op, ...arguments_ } = args;
|
|
38340
38349
|
const bridge = async (command, extra = {}) => {
|
|
@@ -38923,7 +38932,7 @@ class GodotServer {
|
|
|
38923
38932
|
if (!engine.ok) {
|
|
38924
38933
|
return engine.response;
|
|
38925
38934
|
}
|
|
38926
|
-
const classes = await this.
|
|
38935
|
+
const classes = await this.rebuildClassCache(project.value.path);
|
|
38927
38936
|
if (!classes.ok) {
|
|
38928
38937
|
return this.answer(classes);
|
|
38929
38938
|
}
|
|
@@ -39140,7 +39149,8 @@ class GodotServer {
|
|
|
39140
39149
|
}
|
|
39141
39150
|
const stale = unloaded.filter((type) => type.inTheClassCache);
|
|
39142
39151
|
if (stale.length > 0) {
|
|
39143
|
-
|
|
39152
|
+
const remedy = this.editorWritesAShortList(projectPath) ? "editor_rescan will not clear it here: this editor wrote the class cache from a list shorter than the file on its last scan and writes the same list on every scan, so editor_launch restart is what loads it." : "editor_rescan clears it, measured against a real editor with waiting the same length ruled out; editor_launch restart also does, and costs more.";
|
|
39153
|
+
notes.push(`${stale.map((type) => type.type).join(", ")} ${stale.length === 1 ? "is" : "are"} in the class cache and still unresolved here, which is the editor's loaded list being behind rather than anything on disk. A game launched now reads the cache and resolves ${stale.length === 1 ? "it" : "them"}, so the run and the diagnostics disagree. ${remedy}`);
|
|
39144
39154
|
}
|
|
39145
39155
|
return {
|
|
39146
39156
|
...contradicted.length === 0 ? {} : { contradictedByTheFile: contradicted },
|
|
@@ -39148,6 +39158,10 @@ class GodotServer {
|
|
|
39148
39158
|
...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
|
|
39149
39159
|
};
|
|
39150
39160
|
}
|
|
39161
|
+
editorWritesAShortList(projectPath) {
|
|
39162
|
+
const key = resolve5(projectPath);
|
|
39163
|
+
return this.shortListEditors.has(key) && this.shortListEditors.get(key) === this.godotBridge.getStatus().editorPid;
|
|
39164
|
+
}
|
|
39151
39165
|
sourceOfClass(projectPath, resourcePath) {
|
|
39152
39166
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
39153
39167
|
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
@@ -40497,6 +40511,9 @@ class GodotServer {
|
|
|
40497
40511
|
}
|
|
40498
40512
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
40499
40513
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
40514
|
+
if (lost.length > 0) {
|
|
40515
|
+
this.shortListEditors.set(resolve5(projectPath), this.godotBridge.getStatus().editorPid);
|
|
40516
|
+
}
|
|
40500
40517
|
const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
|
|
40501
40518
|
const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
40502
40519
|
const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
|
|
@@ -40552,7 +40569,7 @@ class GodotServer {
|
|
|
40552
40569
|
if (projectPath === "") {
|
|
40553
40570
|
return [];
|
|
40554
40571
|
}
|
|
40555
|
-
const rebuilt = await this.
|
|
40572
|
+
const rebuilt = await this.rebuildClassCache(projectPath);
|
|
40556
40573
|
if (!rebuilt.ok) {
|
|
40557
40574
|
this.logDebug(`Could not rebuild the class cache after a scan: ${rebuilt.message}`);
|
|
40558
40575
|
return [];
|
|
@@ -40560,6 +40577,34 @@ class GodotServer {
|
|
|
40560
40577
|
const now = cachedClasses(projectPath);
|
|
40561
40578
|
return now === null ? [] : lost.filter((name) => now.has(name));
|
|
40562
40579
|
}
|
|
40580
|
+
async rebuildClassCache(projectPath) {
|
|
40581
|
+
const key = resolve5(projectPath);
|
|
40582
|
+
const before = cachedClasses(projectPath);
|
|
40583
|
+
const remembered = this.cacheLastRebuilt.get(key);
|
|
40584
|
+
const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
|
|
40585
|
+
if (!rebuilt.ok) {
|
|
40586
|
+
return rebuilt;
|
|
40587
|
+
}
|
|
40588
|
+
const after = cachedClasses(projectPath);
|
|
40589
|
+
if (after === null) {
|
|
40590
|
+
return rebuilt;
|
|
40591
|
+
}
|
|
40592
|
+
this.cacheLastRebuilt.set(key, new Set(after.keys()));
|
|
40593
|
+
const lost = remembered === undefined || before === null ? [] : [...remembered].filter((name) => !before.has(name) && after.has(name)).sort();
|
|
40594
|
+
if (lost.length === 0 || !this.godotBridge.isConnected()) {
|
|
40595
|
+
return rebuilt;
|
|
40596
|
+
}
|
|
40597
|
+
this.shortListEditors.set(key, this.godotBridge.getStatus().editorPid);
|
|
40598
|
+
const them = lost.length === 1 ? "it" : "them";
|
|
40599
|
+
return {
|
|
40600
|
+
...rebuilt,
|
|
40601
|
+
payload: {
|
|
40602
|
+
...rebuilt.payload,
|
|
40603
|
+
lostSinceLastRebuild: lost,
|
|
40604
|
+
note: `The cache held ${lost.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`
|
|
40605
|
+
}
|
|
40606
|
+
};
|
|
40607
|
+
}
|
|
40563
40608
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
40564
40609
|
const first = answered.content[0];
|
|
40565
40610
|
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
@@ -40570,15 +40615,18 @@ class GodotServer {
|
|
|
40570
40615
|
if (checked.unseen.length === 0 && stillHeld.length === 0 && checked.unchecked === undefined) {
|
|
40571
40616
|
return answered;
|
|
40572
40617
|
}
|
|
40573
|
-
const
|
|
40618
|
+
const payload = asParams(JSON.parse(first.text));
|
|
40619
|
+
const said = readString(payload, "note");
|
|
40620
|
+
const notes = said === undefined ? [] : [said];
|
|
40574
40621
|
if (checked.unseen.length > 0) {
|
|
40575
|
-
|
|
40622
|
+
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
40623
|
+
notes.push(projectPath !== "" && this.editorWritesAShortList(projectPath) ? "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan will not reach it either on this editor, whose last scan wrote the cache from a list shorter than the file and would again; editor_launch restart is what loads them." : "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan does, on its own and with no change to the declaring script; editor_launch restart also does, and costs more.");
|
|
40576
40624
|
}
|
|
40577
40625
|
if (stillHeld.length > 0) {
|
|
40578
40626
|
notes.push(`The editor is still holding ${stillHeld.join(", ")}, which this project no longer declares, and it will write that list back over the cache this call just corrected. The entry returns pointing at a script that is gone, and the next engine to walk get_global_class_list() dies on "File not found". A rescan does not clear this one: it picks up a class that has appeared and does not drop one that has gone. editor_launch restart does.`);
|
|
40579
40627
|
}
|
|
40580
40628
|
return this.jsonTextResponse({
|
|
40581
|
-
...
|
|
40629
|
+
...payload,
|
|
40582
40630
|
unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
|
|
40583
40631
|
stillHeldByEditor: stillHeld.length > 0 ? stillHeld : undefined,
|
|
40584
40632
|
classesUnchecked: checked.unchecked,
|
|
@@ -40871,7 +40919,7 @@ var init_server3 = __esm(() => {
|
|
|
40871
40919
|
|
|
40872
40920
|
// src/cli.ts
|
|
40873
40921
|
import { existsSync as existsSync15 } from "fs";
|
|
40874
|
-
import { join as join21, resolve as
|
|
40922
|
+
import { join as join21, resolve as resolve6 } from "path";
|
|
40875
40923
|
|
|
40876
40924
|
// src/errors.ts
|
|
40877
40925
|
class Refusal extends Error {
|
|
@@ -42575,7 +42623,7 @@ class UsageError extends Refusal {
|
|
|
42575
42623
|
}
|
|
42576
42624
|
function projectArgument(at) {
|
|
42577
42625
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
42578
|
-
const projectPath =
|
|
42626
|
+
const projectPath = resolve6(named ?? ".");
|
|
42579
42627
|
if (!existsSync15(join21(projectPath, "project.godot"))) {
|
|
42580
42628
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
42581
42629
|
}
|
|
@@ -267,23 +267,46 @@ func _announce(bind_address: String) -> void:
|
|
|
267
267
|
)
|
|
268
268
|
return
|
|
269
269
|
var path: String = directory.path_join("runtime-%d.json" % OS.get_process_id())
|
|
270
|
-
|
|
270
|
+
# Written whole under a name no server reads and moved into place, so no look ever meets
|
|
271
|
+
# the file empty: a server sweeping the directory between an open and a write took the
|
|
272
|
+
# announcement for a dead game's leftover, and the directory is shared by every server on
|
|
273
|
+
# the machine, older ones included.
|
|
274
|
+
var staged: String = path + ".tmp"
|
|
275
|
+
var file: FileAccess = FileAccess.open(staged, FileAccess.WRITE)
|
|
271
276
|
if file == null:
|
|
272
277
|
push_warning(
|
|
273
278
|
(
|
|
274
279
|
"[gdharness] cannot write %s (%s); the server will not find this game"
|
|
275
|
-
% [
|
|
280
|
+
% [staged, FileAccess.get_open_error()]
|
|
276
281
|
)
|
|
277
282
|
)
|
|
278
283
|
return
|
|
279
|
-
var
|
|
284
|
+
var written: bool = file.store_string(JSON.stringify(_identity(bind_address)))
|
|
280
285
|
file.close()
|
|
281
|
-
if not
|
|
282
|
-
push_warning("[gdharness] cannot write %s; the server will not find this game" %
|
|
286
|
+
if not written:
|
|
287
|
+
push_warning("[gdharness] cannot write %s; the server will not find this game" % staged)
|
|
288
|
+
_unstage(staged)
|
|
289
|
+
return
|
|
290
|
+
var moved: Error = DirAccess.rename_absolute(staged, path)
|
|
291
|
+
if moved != OK:
|
|
292
|
+
push_warning(
|
|
293
|
+
(
|
|
294
|
+
"[gdharness] cannot move %s to %s (%s); the server will not find this game"
|
|
295
|
+
% [staged, path, moved]
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
_unstage(staged)
|
|
283
299
|
return
|
|
284
300
|
_announcement = path
|
|
285
301
|
|
|
286
302
|
|
|
303
|
+
## Takes a staged announcement down after a write or a move that failed. The answer is taken
|
|
304
|
+
## rather than dropped, since these scripts compile under the game's own warning levels; a
|
|
305
|
+
## staged file that will not go either is nothing any server reads.
|
|
306
|
+
func _unstage(staged: String) -> void:
|
|
307
|
+
var _removed: Error = DirAccess.remove_absolute(staged)
|
|
308
|
+
|
|
309
|
+
|
|
287
310
|
## What the announcement file and the welcome both carry: enough to pick this game out of
|
|
288
311
|
## several and to know whether the server speaks its protocol.
|
|
289
312
|
func _identity(bind_address: String) -> Dictionary:
|
package/build/index.js
CHANGED
|
@@ -10184,7 +10184,7 @@ import {
|
|
|
10184
10184
|
statSync as statSync5
|
|
10185
10185
|
} from "node:fs";
|
|
10186
10186
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10187
|
-
import { basename as basename3, dirname as dirname8, join as join14, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10187
|
+
import { basename as basename3, dirname as dirname8, join as join14, normalize as normalize2, relative as relative3, resolve as resolve5 } from "node:path";
|
|
10188
10188
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10189
10189
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10190
10190
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -29468,7 +29468,7 @@ var TOOL_SPECS = [
|
|
|
29468
29468
|
requires: []
|
|
29469
29469
|
},
|
|
29470
29470
|
refresh_classes: {
|
|
29471
|
-
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
|
|
29471
|
+
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong. A class the cache held after the last rebuild and had lost when this one began is named under lostSinceLastRebuild: the editor wrote the file without it in between, on a save or a scan, and will again until restarted',
|
|
29472
29472
|
requires: []
|
|
29473
29473
|
}
|
|
29474
29474
|
}
|
|
@@ -29493,7 +29493,7 @@ var TOOL_SPECS = [
|
|
|
29493
29493
|
},
|
|
29494
29494
|
{
|
|
29495
29495
|
name: "project_test",
|
|
29496
|
-
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. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. 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.",
|
|
29496
|
+
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, and the rebuild's answer is under classes: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. 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. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. 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.",
|
|
29497
29497
|
parameters: {
|
|
29498
29498
|
projectPath: PROJECT_PATH,
|
|
29499
29499
|
path: {
|
|
@@ -29710,7 +29710,7 @@ var TOOL_SPECS = [
|
|
|
29710
29710
|
},
|
|
29711
29711
|
{
|
|
29712
29712
|
name: "script_diagnostics",
|
|
29713
|
-
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
29713
|
+
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. The one exception is named when it applies: an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
29714
29714
|
parameters: {
|
|
29715
29715
|
projectPath: PROJECT_PATH,
|
|
29716
29716
|
scriptPath: SCRIPT_PATH
|
|
@@ -30658,6 +30658,8 @@ class GodotServer {
|
|
|
30658
30658
|
successorWatch = null;
|
|
30659
30659
|
lastProjectPath = null;
|
|
30660
30660
|
shutdownInitiated = false;
|
|
30661
|
+
shortListEditors = new Map;
|
|
30662
|
+
cacheLastRebuilt = new Map;
|
|
30661
30663
|
launchedEditor = null;
|
|
30662
30664
|
launchedFrom = null;
|
|
30663
30665
|
ownProject;
|
|
@@ -31071,8 +31073,15 @@ class GodotServer {
|
|
|
31071
31073
|
const contained = this.containProjectFiles(asks);
|
|
31072
31074
|
return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
|
|
31073
31075
|
}
|
|
31074
|
-
|
|
31075
|
-
|
|
31076
|
+
if (headless !== "refresh_class_cache") {
|
|
31077
|
+
return await this.headless(headless, answerable);
|
|
31078
|
+
}
|
|
31079
|
+
const project = this.project(answerable);
|
|
31080
|
+
if (!project.ok) {
|
|
31081
|
+
return project.response;
|
|
31082
|
+
}
|
|
31083
|
+
const answered = this.answer(await this.rebuildClassCache(project.value.path));
|
|
31084
|
+
return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
31076
31085
|
}
|
|
31077
31086
|
const { op: _op, ...arguments_ } = args;
|
|
31078
31087
|
const bridge = async (command, extra = {}) => {
|
|
@@ -31661,7 +31670,7 @@ class GodotServer {
|
|
|
31661
31670
|
if (!engine.ok) {
|
|
31662
31671
|
return engine.response;
|
|
31663
31672
|
}
|
|
31664
|
-
const classes = await this.
|
|
31673
|
+
const classes = await this.rebuildClassCache(project.value.path);
|
|
31665
31674
|
if (!classes.ok) {
|
|
31666
31675
|
return this.answer(classes);
|
|
31667
31676
|
}
|
|
@@ -31878,7 +31887,8 @@ class GodotServer {
|
|
|
31878
31887
|
}
|
|
31879
31888
|
const stale = unloaded.filter((type) => type.inTheClassCache);
|
|
31880
31889
|
if (stale.length > 0) {
|
|
31881
|
-
|
|
31890
|
+
const remedy = this.editorWritesAShortList(projectPath) ? "editor_rescan will not clear it here: this editor wrote the class cache from a list shorter than the file on its last scan and writes the same list on every scan, so editor_launch restart is what loads it." : "editor_rescan clears it, measured against a real editor with waiting the same length ruled out; editor_launch restart also does, and costs more.";
|
|
31891
|
+
notes.push(`${stale.map((type) => type.type).join(", ")} ${stale.length === 1 ? "is" : "are"} in the class cache and still unresolved here, which is the editor's loaded list being behind rather than anything on disk. A game launched now reads the cache and resolves ${stale.length === 1 ? "it" : "them"}, so the run and the diagnostics disagree. ${remedy}`);
|
|
31882
31892
|
}
|
|
31883
31893
|
return {
|
|
31884
31894
|
...contradicted.length === 0 ? {} : { contradictedByTheFile: contradicted },
|
|
@@ -31886,6 +31896,10 @@ class GodotServer {
|
|
|
31886
31896
|
...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
|
|
31887
31897
|
};
|
|
31888
31898
|
}
|
|
31899
|
+
editorWritesAShortList(projectPath) {
|
|
31900
|
+
const key = resolve5(projectPath);
|
|
31901
|
+
return this.shortListEditors.has(key) && this.shortListEditors.get(key) === this.godotBridge.getStatus().editorPid;
|
|
31902
|
+
}
|
|
31889
31903
|
sourceOfClass(projectPath, resourcePath) {
|
|
31890
31904
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
31891
31905
|
if (!contained.ok || !existsSync9(contained.absolutePath)) {
|
|
@@ -33235,6 +33249,9 @@ class GodotServer {
|
|
|
33235
33249
|
}
|
|
33236
33250
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
33237
33251
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
33252
|
+
if (lost.length > 0) {
|
|
33253
|
+
this.shortListEditors.set(resolve5(projectPath), this.godotBridge.getStatus().editorPid);
|
|
33254
|
+
}
|
|
33238
33255
|
const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
|
|
33239
33256
|
const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
33240
33257
|
const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
|
|
@@ -33290,7 +33307,7 @@ class GodotServer {
|
|
|
33290
33307
|
if (projectPath === "") {
|
|
33291
33308
|
return [];
|
|
33292
33309
|
}
|
|
33293
|
-
const rebuilt = await this.
|
|
33310
|
+
const rebuilt = await this.rebuildClassCache(projectPath);
|
|
33294
33311
|
if (!rebuilt.ok) {
|
|
33295
33312
|
this.logDebug(`Could not rebuild the class cache after a scan: ${rebuilt.message}`);
|
|
33296
33313
|
return [];
|
|
@@ -33298,6 +33315,34 @@ class GodotServer {
|
|
|
33298
33315
|
const now = cachedClasses(projectPath);
|
|
33299
33316
|
return now === null ? [] : lost.filter((name) => now.has(name));
|
|
33300
33317
|
}
|
|
33318
|
+
async rebuildClassCache(projectPath) {
|
|
33319
|
+
const key = resolve5(projectPath);
|
|
33320
|
+
const before = cachedClasses(projectPath);
|
|
33321
|
+
const remembered = this.cacheLastRebuilt.get(key);
|
|
33322
|
+
const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
|
|
33323
|
+
if (!rebuilt.ok) {
|
|
33324
|
+
return rebuilt;
|
|
33325
|
+
}
|
|
33326
|
+
const after = cachedClasses(projectPath);
|
|
33327
|
+
if (after === null) {
|
|
33328
|
+
return rebuilt;
|
|
33329
|
+
}
|
|
33330
|
+
this.cacheLastRebuilt.set(key, new Set(after.keys()));
|
|
33331
|
+
const lost = remembered === undefined || before === null ? [] : [...remembered].filter((name) => !before.has(name) && after.has(name)).sort();
|
|
33332
|
+
if (lost.length === 0 || !this.godotBridge.isConnected()) {
|
|
33333
|
+
return rebuilt;
|
|
33334
|
+
}
|
|
33335
|
+
this.shortListEditors.set(key, this.godotBridge.getStatus().editorPid);
|
|
33336
|
+
const them = lost.length === 1 ? "it" : "them";
|
|
33337
|
+
return {
|
|
33338
|
+
...rebuilt,
|
|
33339
|
+
payload: {
|
|
33340
|
+
...rebuilt.payload,
|
|
33341
|
+
lostSinceLastRebuild: lost,
|
|
33342
|
+
note: `The cache held ${lost.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`
|
|
33343
|
+
}
|
|
33344
|
+
};
|
|
33345
|
+
}
|
|
33301
33346
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
33302
33347
|
const first = answered.content[0];
|
|
33303
33348
|
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
@@ -33308,15 +33353,18 @@ class GodotServer {
|
|
|
33308
33353
|
if (checked.unseen.length === 0 && stillHeld.length === 0 && checked.unchecked === undefined) {
|
|
33309
33354
|
return answered;
|
|
33310
33355
|
}
|
|
33311
|
-
const
|
|
33356
|
+
const payload = asParams(JSON.parse(first.text));
|
|
33357
|
+
const said = readString(payload, "note");
|
|
33358
|
+
const notes = said === undefined ? [] : [said];
|
|
33312
33359
|
if (checked.unseen.length > 0) {
|
|
33313
|
-
|
|
33360
|
+
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
33361
|
+
notes.push(projectPath !== "" && this.editorWritesAShortList(projectPath) ? "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan will not reach it either on this editor, whose last scan wrote the cache from a list shorter than the file and would again; editor_launch restart is what loads them." : "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan does, on its own and with no change to the declaring script; editor_launch restart also does, and costs more.");
|
|
33314
33362
|
}
|
|
33315
33363
|
if (stillHeld.length > 0) {
|
|
33316
33364
|
notes.push(`The editor is still holding ${stillHeld.join(", ")}, which this project no longer declares, and it will write that list back over the cache this call just corrected. The entry returns pointing at a script that is gone, and the next engine to walk get_global_class_list() dies on "File not found". A rescan does not clear this one: it picks up a class that has appeared and does not drop one that has gone. editor_launch restart does.`);
|
|
33317
33365
|
}
|
|
33318
33366
|
return this.jsonTextResponse({
|
|
33319
|
-
...
|
|
33367
|
+
...payload,
|
|
33320
33368
|
unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
|
|
33321
33369
|
stillHeldByEditor: stillHeld.length > 0 ? stillHeld : undefined,
|
|
33322
33370
|
classesUnchecked: checked.unchecked,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
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",
|