gdharness 1.3.6 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -8367,9 +8367,487 @@ var init_doc_cache = __esm(() => {
8367
8367
  CACHE_FILE = /^editor_doc_cache-\d+\.\d+\.res$/;
8368
8368
  });
8369
8369
 
8370
+ // src/project-scan.ts
8371
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync3 } from "node:fs";
8372
+ import { extname, join as join7 } from "node:path";
8373
+ function skipped(name) {
8374
+ return name.startsWith(".") || SKIPPED.has(name);
8375
+ }
8376
+ function steppedOver(directory) {
8377
+ return gdignored(directory) || installedByUs(directory);
8378
+ }
8379
+ function gdignored(directory) {
8380
+ return existsSync6(join7(directory, ".gdignore"));
8381
+ }
8382
+ function installedByUs(directory) {
8383
+ return existsSync6(join7(directory, VERSION_MARKER));
8384
+ }
8385
+ function projectStructure(projectPath) {
8386
+ const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
8387
+ const visit = (directory) => {
8388
+ if (steppedOver(directory)) {
8389
+ return;
8390
+ }
8391
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8392
+ if (skipped(entry.name)) {
8393
+ continue;
8394
+ }
8395
+ if (entry.isDirectory()) {
8396
+ visit(join7(directory, entry.name));
8397
+ } else if (entry.isFile()) {
8398
+ const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
8399
+ if (SIDECAR_EXTENSIONS.has(extension)) {
8400
+ continue;
8401
+ }
8402
+ if (SCENE_EXTENSIONS.has(extension)) {
8403
+ structure.scenes += 1;
8404
+ } else if (extension === "gd" || extension === "cs") {
8405
+ structure.scripts += 1;
8406
+ } else if (ASSET_EXTENSIONS.has(extension)) {
8407
+ structure.assets += 1;
8408
+ } else {
8409
+ structure.other += 1;
8410
+ }
8411
+ }
8412
+ }
8413
+ };
8414
+ visit(projectPath);
8415
+ return structure;
8416
+ }
8417
+ function projectFiles(projectPath, extensions) {
8418
+ const found = [];
8419
+ const visit = (directory, prefix) => {
8420
+ if (steppedOver(directory)) {
8421
+ return;
8422
+ }
8423
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8424
+ if (skipped(entry.name)) {
8425
+ continue;
8426
+ }
8427
+ const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
8428
+ if (entry.isDirectory()) {
8429
+ visit(join7(directory, entry.name), spelled);
8430
+ } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
8431
+ found.push(spelled);
8432
+ }
8433
+ }
8434
+ };
8435
+ visit(projectPath, "");
8436
+ return found.sort();
8437
+ }
8438
+ function scriptsWithoutUid(projectPath) {
8439
+ const found = [];
8440
+ const visit = (directory, prefix) => {
8441
+ if (steppedOver(directory)) {
8442
+ return;
8443
+ }
8444
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8445
+ if (skipped(entry.name)) {
8446
+ continue;
8447
+ }
8448
+ const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
8449
+ if (entry.isDirectory()) {
8450
+ visit(join7(directory, entry.name), spelled);
8451
+ continue;
8452
+ }
8453
+ if (!entry.isFile()) {
8454
+ continue;
8455
+ }
8456
+ const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
8457
+ if (CARRIES_A_UID.has(extension) && !existsSync6(join7(directory, `${entry.name}.uid`))) {
8458
+ found.push(spelled);
8459
+ }
8460
+ }
8461
+ };
8462
+ visit(projectPath, "");
8463
+ return found.sort();
8464
+ }
8465
+ function pathsInUidCache(projectPath) {
8466
+ let data;
8467
+ try {
8468
+ data = readFileSync3(join7(projectPath, ".godot", "uid_cache.bin"));
8469
+ } catch {
8470
+ return null;
8471
+ }
8472
+ if (data.length < 4) {
8473
+ return null;
8474
+ }
8475
+ const count = data.readUInt32LE(0);
8476
+ const paths = new Map;
8477
+ let at = 4;
8478
+ for (let entry = 0;entry < count; entry++) {
8479
+ if (at + 12 > data.length) {
8480
+ return null;
8481
+ }
8482
+ const id = data.readBigUInt64LE(at);
8483
+ const length = data.readUInt32LE(at + 8);
8484
+ at += 12;
8485
+ if (at + length > data.length) {
8486
+ return null;
8487
+ }
8488
+ paths.set(id, data.toString("utf8", at, at + length));
8489
+ at += length;
8490
+ }
8491
+ return new Set(paths.values());
8492
+ }
8493
+ function isBinary(bytes) {
8494
+ return bytes.subarray(0, 8000).includes(0);
8495
+ }
8496
+ function projectTexts(projectPath) {
8497
+ const found = [];
8498
+ const visit = (directory, prefix) => {
8499
+ if (steppedOver(directory)) {
8500
+ return;
8501
+ }
8502
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8503
+ if (skipped(entry.name)) {
8504
+ continue;
8505
+ }
8506
+ const absolute = join7(directory, entry.name);
8507
+ const spelled = `${prefix}${entry.name}`;
8508
+ if (entry.isDirectory()) {
8509
+ visit(absolute, `${spelled}/`);
8510
+ continue;
8511
+ }
8512
+ const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
8513
+ if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
8514
+ continue;
8515
+ }
8516
+ const bytes = readFileSync3(absolute);
8517
+ if (!isBinary(bytes)) {
8518
+ found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
8519
+ }
8520
+ }
8521
+ };
8522
+ visit(projectPath, "");
8523
+ return found;
8524
+ }
8525
+ function ignoredTexts(projectPath) {
8526
+ const found = [];
8527
+ const visit = (directory, prefix, ignored) => {
8528
+ if (installedByUs(directory)) {
8529
+ return;
8530
+ }
8531
+ const under = ignored || gdignored(directory);
8532
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8533
+ if (skipped(entry.name)) {
8534
+ continue;
8535
+ }
8536
+ const absolute = join7(directory, entry.name);
8537
+ const spelled = `${prefix}${entry.name}`;
8538
+ if (entry.isDirectory()) {
8539
+ visit(absolute, `${spelled}/`, under);
8540
+ continue;
8541
+ }
8542
+ const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
8543
+ if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
8544
+ continue;
8545
+ }
8546
+ const bytes = readFileSync3(absolute);
8547
+ if (!isBinary(bytes)) {
8548
+ found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
8549
+ }
8550
+ }
8551
+ };
8552
+ visit(projectPath, "", false);
8553
+ return found;
8554
+ }
8555
+ function plainMatch(line, needle, caseSensitive) {
8556
+ if (caseSensitive) {
8557
+ return line.includes(needle) ? [needle] : null;
8558
+ }
8559
+ const lowered = line.toLowerCase();
8560
+ const at = lowered.indexOf(needle);
8561
+ if (at < 0) {
8562
+ return null;
8563
+ }
8564
+ return [lowered.length === line.length ? line.slice(at, at + needle.length) : needle];
8565
+ }
8566
+ function compiledQuery(query, caseSensitive) {
8567
+ try {
8568
+ return new RegExp(query, caseSensitive ? "" : "i");
8569
+ } catch (error) {
8570
+ const why = errorMessage(error).split(": ").at(-1) ?? "";
8571
+ throw new Error(`'${query}' is not a regular expression: ${why.charAt(0).toLowerCase()}${why.slice(1)}. With regex true, a character such as ( ) [ ] { } . * + ? | ^ $ or \\ stands for itself only after a backslash, as in \\(; or leave regex out to search for the text as written.`);
8572
+ }
8573
+ }
8574
+ function searchProject(projectPath, options) {
8575
+ const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
8576
+ const result = {
8577
+ query: options.query,
8578
+ results: [],
8579
+ summary: { files_searched: 0, files_with_matches: 0, total_matches: 0, truncated: false }
8580
+ };
8581
+ const regex = options.regex ? compiledQuery(options.query, options.caseSensitive) : null;
8582
+ const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
8583
+ const full = () => result.summary.truncated;
8584
+ const notSearched = [];
8585
+ const visit = (directory) => {
8586
+ if (steppedOver(directory)) {
8587
+ if (gdignored(directory)) {
8588
+ notSearched.push(`res://${directory.slice(projectPath.length + 1).replace(/\\/g, "/")}/`);
8589
+ }
8590
+ return;
8591
+ }
8592
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8593
+ if (full()) {
8594
+ return;
8595
+ }
8596
+ if (skipped(entry.name)) {
8597
+ continue;
8598
+ }
8599
+ const entryPath = join7(directory, entry.name);
8600
+ if (entry.isDirectory()) {
8601
+ visit(entryPath);
8602
+ continue;
8603
+ }
8604
+ const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
8605
+ if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
8606
+ continue;
8607
+ }
8608
+ const bytes = readFileSync3(entryPath);
8609
+ if (extensions === null && isBinary(bytes)) {
8610
+ continue;
8611
+ }
8612
+ result.summary.files_searched += 1;
8613
+ const matches = [];
8614
+ for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
8615
+ const found = regex ? regex.exec(line) : plainMatch(line, needle, options.caseSensitive);
8616
+ if (found === null) {
8617
+ continue;
8618
+ }
8619
+ if (result.summary.total_matches >= options.maxResults) {
8620
+ result.summary.truncated = true;
8621
+ break;
8622
+ }
8623
+ matches.push({ line: index + 1, content: line.trim(), match: found[0] });
8624
+ result.summary.total_matches += 1;
8625
+ }
8626
+ if (matches.length > 0) {
8627
+ const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
8628
+ result.results.push({ file: `res://${relativePath}`, matches });
8629
+ result.summary.files_with_matches += 1;
8630
+ }
8631
+ }
8632
+ };
8633
+ visit(projectPath);
8634
+ return notSearched.length === 0 ? result : { ...result, not_searched: notSearched };
8635
+ }
8636
+ var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, SIDECAR_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
8637
+ var init_project_scan = __esm(() => {
8638
+ init_errors();
8639
+ SKIPPED = new Set(["node_modules"]);
8640
+ SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
8641
+ SIDECAR_EXTENSIONS = new Set(["import", "uid"]);
8642
+ ASSET_EXTENSIONS = new Set([
8643
+ "png",
8644
+ "jpg",
8645
+ "jpeg",
8646
+ "webp",
8647
+ "svg",
8648
+ "bmp",
8649
+ "tga",
8650
+ "exr",
8651
+ "hdr",
8652
+ "ktx",
8653
+ "dds",
8654
+ "ttf",
8655
+ "otf",
8656
+ "woff",
8657
+ "woff2",
8658
+ "fnt",
8659
+ "wav",
8660
+ "mp3",
8661
+ "ogg",
8662
+ "glb",
8663
+ "gltf",
8664
+ "obj",
8665
+ "fbx",
8666
+ "blend",
8667
+ "dae",
8668
+ "ogv",
8669
+ "gdshader",
8670
+ "shader"
8671
+ ]);
8672
+ CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
8673
+ BINARY_EXTENSIONS = new Set([
8674
+ ...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
8675
+ "scn",
8676
+ "res",
8677
+ "zip",
8678
+ "pck",
8679
+ "exe",
8680
+ "dll",
8681
+ "so",
8682
+ "dylib"
8683
+ ]);
8684
+ });
8685
+
8686
+ // src/import-freshness.ts
8687
+ import { createHash } from "node:crypto";
8688
+ import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync4, statSync as statSync5 } from "node:fs";
8689
+ import { join as join8 } from "node:path";
8690
+ function md5Of(file) {
8691
+ let stats;
8692
+ try {
8693
+ stats = statSync5(file);
8694
+ } catch {
8695
+ return null;
8696
+ }
8697
+ const known = hashes.get(file);
8698
+ if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
8699
+ return known.md5;
8700
+ }
8701
+ let md5;
8702
+ try {
8703
+ md5 = createHash("md5").update(readFileSync4(file)).digest("hex");
8704
+ } catch {
8705
+ return null;
8706
+ }
8707
+ hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
8708
+ return md5;
8709
+ }
8710
+ function sectionOf(text, section) {
8711
+ const keys = new Map;
8712
+ let inside = false;
8713
+ for (const line of text.split(/\r?\n/)) {
8714
+ const header = /^\[([^\]]+)\]\s*$/.exec(line);
8715
+ if (header !== null) {
8716
+ inside = header[1] === section;
8717
+ continue;
8718
+ }
8719
+ const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
8720
+ if (pair?.[1] !== undefined && pair[2] !== undefined) {
8721
+ keys.set(pair[1], pair[2]);
8722
+ }
8723
+ }
8724
+ return keys;
8725
+ }
8726
+ function unquoted(value) {
8727
+ return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
8728
+ }
8729
+ function listed(value) {
8730
+ return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
8731
+ }
8732
+ function onDisk(projectPath, resPath) {
8733
+ return join8(projectPath, resPath.slice("res://".length));
8734
+ }
8735
+ function behind(projectPath, resPath, sidecar) {
8736
+ const remap = sectionOf(sidecar, "remap");
8737
+ const importer = unquoted(remap.get("importer"));
8738
+ if (importer === "keep" || importer === "skip") {
8739
+ return null;
8740
+ }
8741
+ const outputs = listed(sectionOf(sidecar, "deps").get("dest_files"));
8742
+ if (outputs.some((output) => !existsSync7(onDisk(projectPath, output)))) {
8743
+ return "its imported output is not on disk";
8744
+ }
8745
+ const name = resPath.slice(resPath.lastIndexOf("/") + 1);
8746
+ const record = join8(projectPath, ".godot", "imported", `${name}-${createHash("md5").update(resPath, "utf8").digest("hex")}.md5`);
8747
+ let recorded = "";
8748
+ try {
8749
+ recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync4(record, "utf8"))?.[1] ?? "";
8750
+ } catch {
8751
+ recorded = "";
8752
+ }
8753
+ if (recorded === "") {
8754
+ return "its import recorded no hash of its source";
8755
+ }
8756
+ const current = md5Of(onDisk(projectPath, resPath));
8757
+ return current === null || current === recorded ? null : "the source changed after it was imported";
8758
+ }
8759
+ function staleImports(projectPath) {
8760
+ const stale = [];
8761
+ const visit = (directory, resDirectory) => {
8762
+ if (existsSync7(join8(directory, ".gdignore")) || existsSync7(join8(directory, VERSION_MARKER))) {
8763
+ return;
8764
+ }
8765
+ let entries;
8766
+ try {
8767
+ entries = readdirSync5(directory, { withFileTypes: true });
8768
+ } catch {
8769
+ return;
8770
+ }
8771
+ const names = new Set(entries.map((entry) => entry.name));
8772
+ for (const entry of entries) {
8773
+ if (entry.name.startsWith(".")) {
8774
+ continue;
8775
+ }
8776
+ const resPath = `${resDirectory}${entry.name}`;
8777
+ if (entry.isDirectory()) {
8778
+ visit(join8(directory, entry.name), `${resPath}/`);
8779
+ continue;
8780
+ }
8781
+ if (!entry.isFile()) {
8782
+ continue;
8783
+ }
8784
+ if (entry.name.endsWith(".import")) {
8785
+ const source = resPath.slice(0, -".import".length);
8786
+ if (!names.has(entry.name.slice(0, -".import".length))) {
8787
+ continue;
8788
+ }
8789
+ let sidecar;
8790
+ try {
8791
+ sidecar = readFileSync4(join8(directory, entry.name), "utf8");
8792
+ } catch {
8793
+ continue;
8794
+ }
8795
+ const reason = behind(projectPath, source, sidecar);
8796
+ if (reason !== null) {
8797
+ stale.push({ path: source, reason });
8798
+ }
8799
+ continue;
8800
+ }
8801
+ const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
8802
+ if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
8803
+ stale.push({ path: resPath, reason: "it has not been imported" });
8804
+ }
8805
+ }
8806
+ };
8807
+ visit(projectPath, "res://");
8808
+ return stale.sort((one, other) => one.path.localeCompare(other.path));
8809
+ }
8810
+ var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
8811
+ var init_import_freshness = __esm(() => {
8812
+ init_project_scan();
8813
+ IMPORTABLE_EXTENSIONS = [
8814
+ "png",
8815
+ "jpg",
8816
+ "jpeg",
8817
+ "webp",
8818
+ "svg",
8819
+ "bmp",
8820
+ "tga",
8821
+ "exr",
8822
+ "hdr",
8823
+ "dds",
8824
+ "ktx",
8825
+ "wav",
8826
+ "mp3",
8827
+ "ogg",
8828
+ "ttf",
8829
+ "otf",
8830
+ "ttc",
8831
+ "otc",
8832
+ "woff",
8833
+ "woff2",
8834
+ "pfb",
8835
+ "pfm",
8836
+ "fnt",
8837
+ "font",
8838
+ "glb",
8839
+ "gltf",
8840
+ "fbx",
8841
+ "obj",
8842
+ "dae"
8843
+ ];
8844
+ IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
8845
+ hashes = new Map;
8846
+ });
8847
+
8370
8848
  // src/library-copies.ts
8371
- import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync3, renameSync, unlinkSync } from "node:fs";
8372
- import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute3, join as join7 } from "node:path";
8849
+ import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync5, renameSync, unlinkSync } from "node:fs";
8850
+ import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute3, join as join9 } from "node:path";
8373
8851
  function freeLibraryCopies(projectPath, platform = process.platform) {
8374
8852
  const setAside = [];
8375
8853
  const swept = [];
@@ -8379,24 +8857,24 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
8379
8857
  for (const library of windowsLibraries(projectPath)) {
8380
8858
  const folder = dirname6(library);
8381
8859
  const copy = `~${basename5(library)}`;
8382
- if (existsSync6(join7(folder, copy))) {
8383
- const aside = join7(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
8860
+ if (existsSync8(join9(folder, copy))) {
8861
+ const aside = join9(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
8384
8862
  try {
8385
- renameSync(join7(folder, copy), aside);
8863
+ renameSync(join9(folder, copy), aside);
8386
8864
  setAside.push(aside);
8387
8865
  } catch {}
8388
8866
  }
8389
8867
  let names;
8390
8868
  try {
8391
- names = readdirSync4(folder);
8869
+ names = readdirSync6(folder);
8392
8870
  } catch {
8393
8871
  continue;
8394
8872
  }
8395
8873
  for (const name of names) {
8396
8874
  if (name.length > copy.length && name.toLowerCase().startsWith(copy.toLowerCase())) {
8397
8875
  try {
8398
- unlinkSync(join7(folder, name));
8399
- swept.push(join7(folder, name));
8876
+ unlinkSync(join9(folder, name));
8877
+ swept.push(join9(folder, name));
8400
8878
  } catch {}
8401
8879
  }
8402
8880
  }
@@ -8404,7 +8882,7 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
8404
8882
  return { setAside, swept };
8405
8883
  }
8406
8884
  function windowsLibraries(projectPath) {
8407
- const listed = readOrNull(join7(projectPath, ".godot", "extension_list.cfg"));
8885
+ const listed = readOrNull(join9(projectPath, ".godot", "extension_list.cfg"));
8408
8886
  if (listed === null) {
8409
8887
  return [];
8410
8888
  }
@@ -8436,13 +8914,13 @@ function resolveResource(projectPath, relativeTo, path) {
8436
8914
  return null;
8437
8915
  }
8438
8916
  if (path.startsWith("res://")) {
8439
- return join7(projectPath, path.slice("res://".length));
8917
+ return join9(projectPath, path.slice("res://".length));
8440
8918
  }
8441
- return isAbsolute3(path) ? path : join7(relativeTo, path);
8919
+ return isAbsolute3(path) ? path : join9(relativeTo, path);
8442
8920
  }
8443
8921
  function readOrNull(path) {
8444
8922
  try {
8445
- return readFileSync3(path, "utf8");
8923
+ return readFileSync5(path, "utf8");
8446
8924
  } catch {
8447
8925
  return null;
8448
8926
  }
@@ -8450,23 +8928,23 @@ function readOrNull(path) {
8450
8928
  var init_library_copies = () => {};
8451
8929
 
8452
8930
  // src/reimport.ts
8453
- import { createHash } from "node:crypto";
8454
- import { existsSync as existsSync7, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync5, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
8455
- import { basename as basename6, join as join8 } from "node:path";
8931
+ import { createHash as createHash2 } from "node:crypto";
8932
+ import { existsSync as existsSync9, readFileSync as readFileSync6, rmSync as rmSync3, statSync as statSync6, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
8933
+ import { basename as basename6, join as join10 } from "node:path";
8456
8934
  function forceNextImport(projectPath, resourcePath) {
8457
8935
  const relative = resourcePath.replace(/^res:\/\//, "");
8458
- const sidecar = join8(projectPath, `${relative}.import`);
8459
- if (!existsSync7(sidecar)) {
8936
+ const sidecar = join10(projectPath, `${relative}.import`);
8937
+ if (!existsSync9(sidecar)) {
8460
8938
  return;
8461
8939
  }
8462
- const { atime, mtimeMs } = statSync5(sidecar);
8463
- writeFileSync3(sidecar, readFileSync4(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
8940
+ const { atime, mtimeMs } = statSync6(sidecar);
8941
+ writeFileSync3(sidecar, readFileSync6(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
8464
8942
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
8465
8943
  rmSync3(importRecord(projectPath, resourcePath), { force: true });
8466
8944
  }
8467
8945
  function importRecord(projectPath, resourcePath) {
8468
- const digest = createHash("md5").update(resourcePath).digest("hex");
8469
- return join8(projectPath, ".godot", "imported", `${basename6(resourcePath)}-${digest}.md5`);
8946
+ const digest = createHash2("md5").update(resourcePath).digest("hex");
8947
+ return join10(projectPath, ".godot", "imported", `${basename6(resourcePath)}-${digest}.md5`);
8470
8948
  }
8471
8949
  var init_reimport = () => {};
8472
8950
 
@@ -8496,17 +8974,17 @@ function alive(pid) {
8496
8974
  var init_alive = () => {};
8497
8975
 
8498
8976
  // src/scratch.ts
8499
- import { existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4 } from "node:fs";
8977
+ import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync4 } from "node:fs";
8500
8978
  import { tmpdir as tmpdir2 } from "node:os";
8501
- import { join as join9 } from "node:path";
8979
+ import { join as join11 } from "node:path";
8502
8980
  import process8 from "node:process";
8503
8981
  function scratchDirectory(kind) {
8504
- return mkdtempSync(join9(tmpdir2(), `gdharness-${kind}-${process8.pid}-`));
8982
+ return mkdtempSync(join11(tmpdir2(), `gdharness-${kind}-${process8.pid}-`));
8505
8983
  }
8506
8984
  function sweepAbandonedScratch(directory = tmpdir2()) {
8507
8985
  let entries;
8508
8986
  try {
8509
- entries = readdirSync5(directory);
8987
+ entries = readdirSync7(directory);
8510
8988
  } catch {
8511
8989
  return;
8512
8990
  }
@@ -8517,7 +8995,7 @@ function sweepAbandonedScratch(directory = tmpdir2()) {
8517
8995
  continue;
8518
8996
  }
8519
8997
  try {
8520
- remove(join9(directory, entry));
8998
+ remove(join11(directory, entry));
8521
8999
  } catch {}
8522
9000
  }
8523
9001
  }
@@ -8555,7 +9033,7 @@ function tryAgain(path, attempt) {
8555
9033
  lastTryOnExit();
8556
9034
  }
8557
9035
  const after = AGAIN_AFTER_MS[attempt];
8558
- if (after === undefined || !existsSync8(path)) {
9036
+ if (after === undefined || !existsSync10(path)) {
8559
9037
  waiting.delete(path);
8560
9038
  if (after === undefined) {
8561
9039
  atExit.add(path);
@@ -8744,6 +9222,29 @@ function ignoredYetRan(report, ignored) {
8744
9222
  }
8745
9223
  return found;
8746
9224
  }
9225
+ function ignoredLeftOut(report, ignored, declared) {
9226
+ const nameOf = (path) => (path.split("/").at(-1) ?? path).replace(/\.gd$/, "");
9227
+ const leftOut = new Set;
9228
+ const unmatched = [];
9229
+ for (const entry of ignored) {
9230
+ const bare = entry.replace("res://", "");
9231
+ const at = bare.lastIndexOf(":");
9232
+ const suiteNamed = at === -1 ? bare : bare.slice(0, at);
9233
+ const testNamed = at === -1 ? null : bare.slice(at + 1);
9234
+ const named = declared.filter((suite) => nameOf(suite.path) === suiteNamed || suite.path.includes(suiteNamed)).flatMap((suite) => suite.cases.filter((one) => testNamed === null || one === testNamed).map((test) => ({ suite, test })));
9235
+ if (named.length === 0) {
9236
+ unmatched.push(entry);
9237
+ continue;
9238
+ }
9239
+ for (const { suite, test } of named) {
9240
+ const reported = report.suites.filter((ran) => ran.path === suite.path || ran.path === null && ran.name === nameOf(suite.path)).some((ran) => ran.cases.some((one) => one.name === test || new RegExp(`^${test}\\W`).test(one.name)));
9241
+ if (!reported) {
9242
+ leftOut.add(`${nameOf(suite.path)}:${test}`);
9243
+ }
9244
+ }
9245
+ }
9246
+ return { leftOut: [...leftOut].sort(), unmatched };
9247
+ }
8747
9248
  function caseOf(suite, element) {
8748
9249
  const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
8749
9250
  const detail = child.text.trim();
@@ -8776,6 +9277,7 @@ function parseJUnit(xml) {
8776
9277
  throw new MalformedReportError("No <testsuites> or <testsuite> in the report");
8777
9278
  }
8778
9279
  const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
9280
+ const where = (suite) => suite.path ?? suite.name;
8779
9281
  const suites = suiteElements.map((suite) => {
8780
9282
  const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
8781
9283
  const failures = cases.filter(failedAnAssertion).length;
@@ -8795,6 +9297,7 @@ function parseJUnit(xml) {
8795
9297
  cases
8796
9298
  };
8797
9299
  });
9300
+ suites.sort((a, b) => where(a) < where(b) ? -1 : where(a) > where(b) ? 1 : 0);
8798
9301
  const all = suites.flatMap((suite) => suite.cases);
8799
9302
  return {
8800
9303
  tests: all.length,
@@ -9102,7 +9605,7 @@ var init_junit = __esm(() => {
9102
9605
  });
9103
9606
 
9104
9607
  // src/script-origins.ts
9105
- import { join as join10 } from "node:path";
9608
+ import { join as join12 } from "node:path";
9106
9609
  function unresolvedClassIn(message) {
9107
9610
  return UNRESOLVED.exec(message)?.[1] ?? null;
9108
9611
  }
@@ -9164,7 +9667,7 @@ function checkWithEngine(godotPath, projectPath) {
9164
9667
  const ran = await runEngine(godotPath, [
9165
9668
  "--headless",
9166
9669
  "--log-file",
9167
- join10(logDir, "engine.log"),
9670
+ join12(logDir, "engine.log"),
9168
9671
  "--path",
9169
9672
  projectPath,
9170
9673
  "--check-only",
@@ -9210,8 +9713,8 @@ var init_script_origins = __esm(() => {
9210
9713
  });
9211
9714
 
9212
9715
  // src/headless.ts
9213
- import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
9214
- import { dirname as dirname7, join as join11 } from "node:path";
9716
+ import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
9717
+ import { dirname as dirname7, join as join13 } from "node:path";
9215
9718
  function snakeCased(params) {
9216
9719
  const result = emptyRecord();
9217
9720
  for (const [key, value] of Object.entries(params)) {
@@ -9223,8 +9726,10 @@ function snakeCased(params) {
9223
9726
  function engineExtras(outcome) {
9224
9727
  const after = outcome.afterAnswerMessages ?? [];
9225
9728
  const before = outcome.beforeOperationMessages ?? [];
9729
+ const notImported = notImportedNote(outcome.messages);
9226
9730
  return {
9227
9731
  ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
9732
+ ...notImported === undefined ? {} : { engine_messages_note: notImported },
9228
9733
  ...before.length > 0 ? {
9229
9734
  engine_messages_before_operation: before,
9230
9735
  engine_messages_before_operation_note: withNotCompiled(BEFORE_OPERATION_NOTE, before)
@@ -9243,6 +9748,20 @@ function withNotCompiled(note, entries) {
9243
9748
  const path = entries.map(notCompiledIn).find((found) => found !== undefined);
9244
9749
  return path === undefined ? note : `${note} ${path} did not compile, so every run of this project starts its autoloads without it until it is fixed; its errors are among these.`;
9245
9750
  }
9751
+ function notImportedNote(entries) {
9752
+ const paths = [
9753
+ ...new Set(entries.flatMap((entry) => {
9754
+ const path = /^No loader found for resource: (res:\/\/\S+)/.exec(entry.text)?.[1];
9755
+ const extension = path?.split(".").pop()?.toLowerCase() ?? "";
9756
+ return path !== undefined && IMPORTABLE_EXTENSIONS.includes(extension) ? [path] : [];
9757
+ }))
9758
+ ];
9759
+ if (paths.length === 0) {
9760
+ return;
9761
+ }
9762
+ const one = paths.length === 1;
9763
+ return `${paths.join(", ")} ${one ? "has" : "have"} no import the engine can load, which is how a file it imports stands before it is first imported, so what uses ${one ? "it" : "them"} reads ${one ? "it" : "them"} as missing although ${one ? "it is" : "they are"} on disk: project_import reimport imports ${one ? "it" : "them"}.`;
9764
+ }
9246
9765
  function withoutAsides(entries) {
9247
9766
  const kept = [];
9248
9767
  let aside = false;
@@ -9349,7 +9868,7 @@ async function importOnce(godotPath, projectPath) {
9349
9868
  ran = await runEngine(godotPath, [
9350
9869
  "--headless",
9351
9870
  "--log-file",
9352
- join11(logDir, "engine.log"),
9871
+ join13(logDir, "engine.log"),
9353
9872
  "--path",
9354
9873
  projectPath,
9355
9874
  "--import"
@@ -9371,13 +9890,13 @@ async function importOnce(godotPath, projectPath) {
9371
9890
  }
9372
9891
  async function runOperation(engine, operation, params, projectPath) {
9373
9892
  const paramsDir = scratchDirectory("params");
9374
- const paramsFile = join11(paramsDir, `${operation}.json`);
9375
- const answerFile = join11(paramsDir, "answer.json");
9893
+ const paramsFile = join13(paramsDir, `${operation}.json`);
9894
+ const answerFile = join13(paramsDir, "answer.json");
9376
9895
  writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
9377
9896
  const args = [
9378
9897
  "--headless",
9379
9898
  "--log-file",
9380
- join11(paramsDir, "engine.log"),
9899
+ join13(paramsDir, "engine.log"),
9381
9900
  "--path",
9382
9901
  projectPath,
9383
9902
  "--script",
@@ -9465,10 +9984,10 @@ async function runOperation(engine, operation, params, projectPath) {
9465
9984
  };
9466
9985
  }
9467
9986
  function answerIn(file) {
9468
- if (!existsSync9(file)) {
9987
+ if (!existsSync11(file)) {
9469
9988
  return { kind: "none" };
9470
9989
  }
9471
- const text = readFileSync5(file, "utf8");
9990
+ const text = readFileSync7(file, "utf8");
9472
9991
  try {
9473
9992
  const parsed = JSON.parse(text);
9474
9993
  return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { kind: "answer", payload: parsed } : { kind: "unreadable", why: `it is JSON but not an object: ${text.slice(0, 200)}` };
@@ -9483,6 +10002,7 @@ var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, wh
9483
10002
  var init_headless = __esm(() => {
9484
10003
  init_doc_cache();
9485
10004
  init_engine_run();
10005
+ init_import_freshness();
9486
10006
  init_library_copies();
9487
10007
  init_paths();
9488
10008
  init_reimport();
@@ -9491,9 +10011,9 @@ var init_headless = __esm(() => {
9491
10011
  });
9492
10012
 
9493
10013
  // src/update-check.ts
9494
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
10014
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
9495
10015
  import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
9496
- import { join as join12 } from "node:path";
10016
+ import { join as join14 } from "node:path";
9497
10017
  function packageUrl(root) {
9498
10018
  return `${root.replace(/\/+$/, "")}/gdharness/latest`;
9499
10019
  }
@@ -9515,28 +10035,28 @@ function cacheDirectory(environment) {
9515
10035
  };
9516
10036
  const home = set("HOME") ?? homedir4();
9517
10037
  if (process.platform === "win32") {
9518
- return join12(set("LOCALAPPDATA") ?? home, "gdharness");
10038
+ return join14(set("LOCALAPPDATA") ?? home, "gdharness");
9519
10039
  }
9520
10040
  if (process.platform === "darwin") {
9521
- return join12(home, "Library", "Caches", "gdharness");
10041
+ return join14(home, "Library", "Caches", "gdharness");
9522
10042
  }
9523
- return join12(set("XDG_CACHE_HOME") ?? join12(home, ".cache"), "gdharness");
10043
+ return join14(set("XDG_CACHE_HOME") ?? join14(home, ".cache"), "gdharness");
9524
10044
  }
9525
10045
  function cacheFile(environment = process.env) {
9526
10046
  try {
9527
10047
  const directory = cacheDirectory(environment);
9528
10048
  mkdirSync3(directory, { recursive: true, mode: 448 });
9529
- return join12(directory, "update-check.json");
10049
+ return join14(directory, "update-check.json");
9530
10050
  } catch {
9531
- return join12(tmpdir3(), "gdharness-update-check.json");
10051
+ return join14(tmpdir3(), "gdharness-update-check.json");
9532
10052
  }
9533
10053
  }
9534
10054
  function readCache(path) {
9535
10055
  try {
9536
- if (!existsSync10(path)) {
10056
+ if (!existsSync12(path)) {
9537
10057
  return null;
9538
10058
  }
9539
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
10059
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
9540
10060
  if (typeof parsed !== "object" || parsed === null) {
9541
10061
  return null;
9542
10062
  }
@@ -9684,7 +10204,7 @@ var init_update_check = __esm(() => {
9684
10204
  });
9685
10205
 
9686
10206
  // src/server-version.ts
9687
- import { readFileSync as readFileSync7 } from "node:fs";
10207
+ import { readFileSync as readFileSync9 } from "node:fs";
9688
10208
  function staleAddon(addonDigest, shippedDigest) {
9689
10209
  return addonDigest !== shippedDigest;
9690
10210
  }
@@ -9743,7 +10263,7 @@ var init_server_version = __esm(() => {
9743
10263
  DEBUG_MODE = process.env["GDHARNESS_DEBUG"] === "1";
9744
10264
  SERVER_VERSION = (() => {
9745
10265
  try {
9746
- const pkg = JSON.parse(readFileSync7(new URL("../package.json", import.meta.url), "utf8"));
10266
+ const pkg = JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8"));
9747
10267
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
9748
10268
  } catch {
9749
10269
  return "0.0.0";
@@ -9752,7 +10272,7 @@ var init_server_version = __esm(() => {
9752
10272
  });
9753
10273
 
9754
10274
  // src/issues.ts
9755
- import { createHash as createHash2 } from "node:crypto";
10275
+ import { createHash as createHash3 } from "node:crypto";
9756
10276
  import process9 from "node:process";
9757
10277
  function feedbackNotice(every) {
9758
10278
  return JSON.stringify({
@@ -9771,7 +10291,7 @@ function runtime() {
9771
10291
  function defectSignature(where, message) {
9772
10292
  const normalised = `${where}
9773
10293
  ${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
9774
- return createHash2("sha256").update(normalised).digest("hex").slice(0, 8);
10294
+ return createHash3("sha256").update(normalised).digest("hex").slice(0, 8);
9775
10295
  }
9776
10296
  function facts(where, message, godotVersion) {
9777
10297
  return [
@@ -10528,8 +11048,8 @@ var init_gdscript_source = __esm(() => {
10528
11048
  });
10529
11049
 
10530
11050
  // src/class-cache.ts
10531
- import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "node:fs";
10532
- import { join as join13, posix } from "node:path";
11051
+ import { existsSync as existsSync13, readdirSync as readdirSync8, readFileSync as readFileSync10, statSync as statSync7 } from "node:fs";
11052
+ import { join as join15, posix } from "node:path";
10533
11053
  function isToolScript(source) {
10534
11054
  return TOOL.test(codeOf(source));
10535
11055
  }
@@ -10548,18 +11068,18 @@ function declaredClassOf(source) {
10548
11068
  }
10549
11069
  function eachScript(projectPath, each) {
10550
11070
  const visit = (directory, prefix) => {
10551
- if (existsSync11(join13(directory, ".gdignore"))) {
11071
+ if (existsSync13(join15(directory, ".gdignore"))) {
10552
11072
  return;
10553
11073
  }
10554
- for (const entry of readdirSync6(directory, { withFileTypes: true })) {
11074
+ for (const entry of readdirSync8(directory, { withFileTypes: true })) {
10555
11075
  if (entry.name.startsWith(".")) {
10556
11076
  continue;
10557
11077
  }
10558
- const path = join13(directory, entry.name);
11078
+ const path = join15(directory, entry.name);
10559
11079
  if (entry.isDirectory()) {
10560
11080
  visit(path, `${prefix}${entry.name}/`);
10561
11081
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
10562
- each(`res://${prefix}${entry.name}`, readFileSync8(path, "utf8"));
11082
+ each(`res://${prefix}${entry.name}`, readFileSync10(path, "utf8"));
10563
11083
  }
10564
11084
  }
10565
11085
  };
@@ -10581,12 +11101,12 @@ function scriptsNaming(projectPath, classes) {
10581
11101
  return naming.sort((a, b) => a.script.localeCompare(b.script));
10582
11102
  }
10583
11103
  function cachedClasses(projectPath) {
10584
- const cache = join13(projectPath, ".godot", "global_script_class_cache.cfg");
10585
- if (!existsSync11(cache)) {
11104
+ const cache = join15(projectPath, ".godot", "global_script_class_cache.cfg");
11105
+ if (!existsSync13(cache)) {
10586
11106
  return null;
10587
11107
  }
10588
11108
  const listed = new Map;
10589
- const text = readFileSync8(cache, "utf8");
11109
+ const text = readFileSync10(cache, "utf8");
10590
11110
  for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
10591
11111
  listed.set(entry[1] ?? "", entry[2] ?? "");
10592
11112
  }
@@ -10879,14 +11399,14 @@ function uncachedForTheEngineNote(projectPath, types) {
10879
11399
  }
10880
11400
  const one = types.length === 1;
10881
11401
  const named = types.map((type) => `${type.type} (${type.declaredIn})`).join(", ");
10882
- const cacheExists = existsSync11(join13(projectPath, ".godot", "global_script_class_cache.cfg"));
11402
+ const cacheExists = existsSync13(join15(projectPath, ".godot", "global_script_class_cache.cfg"));
10883
11403
  const state = cacheExists ? `missing from .godot/global_script_class_cache.cfg, which is where the engine that compiled these scripts resolves a class name from, so the error is the cache being behind the files rather than the script.` : `the engine that compiled these scripts resolves a class name from .godot/global_script_class_cache.cfg, which has not been written yet, as in a project no editor has opened, so the error is the missing cache rather than the script.`;
10884
11404
  return `${named} ${one ? "is" : "are"} declared in this project${cacheExists ? " and" : ", but"} ${state} ` + "project_import refresh_classes writes the cache from the declarations on disk; editor_run start " + "writes it before a game, and an editor opening the project writes it when it scans.";
10885
11405
  }
10886
11406
  function cacheWrittenAt(projectPath) {
10887
- const cache = join13(projectPath, ".godot", "global_script_class_cache.cfg");
11407
+ const cache = join15(projectPath, ".godot", "global_script_class_cache.cfg");
10888
11408
  try {
10889
- return statSync6(cache).mtimeMs;
11409
+ return statSync7(cache).mtimeMs;
10890
11410
  } catch {
10891
11411
  return null;
10892
11412
  }
@@ -10898,14 +11418,14 @@ function declaredSince(projectPath, paths, names, since) {
10898
11418
  return false;
10899
11419
  }
10900
11420
  try {
10901
- return statSync6(join13(projectPath, path.slice("res://".length))).mtimeMs > since;
11421
+ return statSync7(join15(projectPath, path.slice("res://".length))).mtimeMs > since;
10902
11422
  } catch {
10903
11423
  return false;
10904
11424
  }
10905
11425
  });
10906
11426
  }
10907
11427
  function cachedAtMissingPaths(cached, projectPath) {
10908
- return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync11(join13(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
11428
+ return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync13(join15(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
10909
11429
  }
10910
11430
  function staleAgainst(cached, projectPath) {
10911
11431
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
@@ -10929,314 +11449,6 @@ var init_class_cache = __esm(() => {
10929
11449
  TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
10930
11450
  });
10931
11451
 
10932
- // src/project-scan.ts
10933
- import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "node:fs";
10934
- import { extname, join as join14 } from "node:path";
10935
- function skipped(name) {
10936
- return name.startsWith(".") || SKIPPED.has(name);
10937
- }
10938
- function steppedOver(directory) {
10939
- return gdignored(directory) || installedByUs(directory);
10940
- }
10941
- function gdignored(directory) {
10942
- return existsSync12(join14(directory, ".gdignore"));
10943
- }
10944
- function installedByUs(directory) {
10945
- return existsSync12(join14(directory, VERSION_MARKER));
10946
- }
10947
- function projectStructure(projectPath) {
10948
- const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
10949
- const visit = (directory) => {
10950
- if (steppedOver(directory)) {
10951
- return;
10952
- }
10953
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
10954
- if (skipped(entry.name)) {
10955
- continue;
10956
- }
10957
- if (entry.isDirectory()) {
10958
- visit(join14(directory, entry.name));
10959
- } else if (entry.isFile()) {
10960
- const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
10961
- if (SCENE_EXTENSIONS.has(extension)) {
10962
- structure.scenes += 1;
10963
- } else if (extension === "gd" || extension === "cs") {
10964
- structure.scripts += 1;
10965
- } else if (ASSET_EXTENSIONS.has(extension)) {
10966
- structure.assets += 1;
10967
- } else {
10968
- structure.other += 1;
10969
- }
10970
- }
10971
- }
10972
- };
10973
- visit(projectPath);
10974
- return structure;
10975
- }
10976
- function projectFiles(projectPath, extensions) {
10977
- const found = [];
10978
- const visit = (directory, prefix) => {
10979
- if (steppedOver(directory)) {
10980
- return;
10981
- }
10982
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
10983
- if (skipped(entry.name)) {
10984
- continue;
10985
- }
10986
- const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
10987
- if (entry.isDirectory()) {
10988
- visit(join14(directory, entry.name), spelled);
10989
- } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
10990
- found.push(spelled);
10991
- }
10992
- }
10993
- };
10994
- visit(projectPath, "");
10995
- return found.sort();
10996
- }
10997
- function scriptsWithoutUid(projectPath) {
10998
- const found = [];
10999
- const visit = (directory, prefix) => {
11000
- if (steppedOver(directory)) {
11001
- return;
11002
- }
11003
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11004
- if (skipped(entry.name)) {
11005
- continue;
11006
- }
11007
- const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
11008
- if (entry.isDirectory()) {
11009
- visit(join14(directory, entry.name), spelled);
11010
- continue;
11011
- }
11012
- if (!entry.isFile()) {
11013
- continue;
11014
- }
11015
- const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
11016
- if (CARRIES_A_UID.has(extension) && !existsSync12(join14(directory, `${entry.name}.uid`))) {
11017
- found.push(spelled);
11018
- }
11019
- }
11020
- };
11021
- visit(projectPath, "");
11022
- return found.sort();
11023
- }
11024
- function pathsInUidCache(projectPath) {
11025
- let data;
11026
- try {
11027
- data = readFileSync9(join14(projectPath, ".godot", "uid_cache.bin"));
11028
- } catch {
11029
- return null;
11030
- }
11031
- if (data.length < 4) {
11032
- return null;
11033
- }
11034
- const count = data.readUInt32LE(0);
11035
- const paths = new Map;
11036
- let at = 4;
11037
- for (let entry = 0;entry < count; entry++) {
11038
- if (at + 12 > data.length) {
11039
- return null;
11040
- }
11041
- const id = data.readBigUInt64LE(at);
11042
- const length = data.readUInt32LE(at + 8);
11043
- at += 12;
11044
- if (at + length > data.length) {
11045
- return null;
11046
- }
11047
- paths.set(id, data.toString("utf8", at, at + length));
11048
- at += length;
11049
- }
11050
- return new Set(paths.values());
11051
- }
11052
- function isBinary(bytes) {
11053
- return bytes.subarray(0, 8000).includes(0);
11054
- }
11055
- function projectTexts(projectPath) {
11056
- const found = [];
11057
- const visit = (directory, prefix) => {
11058
- if (steppedOver(directory)) {
11059
- return;
11060
- }
11061
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11062
- if (skipped(entry.name)) {
11063
- continue;
11064
- }
11065
- const absolute = join14(directory, entry.name);
11066
- const spelled = `${prefix}${entry.name}`;
11067
- if (entry.isDirectory()) {
11068
- visit(absolute, `${spelled}/`);
11069
- continue;
11070
- }
11071
- const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
11072
- if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
11073
- continue;
11074
- }
11075
- const bytes = readFileSync9(absolute);
11076
- if (!isBinary(bytes)) {
11077
- found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
11078
- }
11079
- }
11080
- };
11081
- visit(projectPath, "");
11082
- return found;
11083
- }
11084
- function ignoredTexts(projectPath) {
11085
- const found = [];
11086
- const visit = (directory, prefix, ignored) => {
11087
- if (installedByUs(directory)) {
11088
- return;
11089
- }
11090
- const under = ignored || gdignored(directory);
11091
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11092
- if (skipped(entry.name)) {
11093
- continue;
11094
- }
11095
- const absolute = join14(directory, entry.name);
11096
- const spelled = `${prefix}${entry.name}`;
11097
- if (entry.isDirectory()) {
11098
- visit(absolute, `${spelled}/`, under);
11099
- continue;
11100
- }
11101
- const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
11102
- if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
11103
- continue;
11104
- }
11105
- const bytes = readFileSync9(absolute);
11106
- if (!isBinary(bytes)) {
11107
- found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
11108
- }
11109
- }
11110
- };
11111
- visit(projectPath, "", false);
11112
- return found;
11113
- }
11114
- function plainMatch(line, needle, caseSensitive) {
11115
- if (caseSensitive) {
11116
- return line.includes(needle) ? [needle] : null;
11117
- }
11118
- const lowered = line.toLowerCase();
11119
- const at = lowered.indexOf(needle);
11120
- if (at < 0) {
11121
- return null;
11122
- }
11123
- return [lowered.length === line.length ? line.slice(at, at + needle.length) : needle];
11124
- }
11125
- function compiledQuery(query, caseSensitive) {
11126
- try {
11127
- return new RegExp(query, caseSensitive ? "" : "i");
11128
- } catch (error) {
11129
- const why = errorMessage(error).split(": ").at(-1) ?? "";
11130
- throw new Error(`'${query}' is not a regular expression: ${why.charAt(0).toLowerCase()}${why.slice(1)}. With regex true, a character such as ( ) [ ] { } . * + ? | ^ $ or \\ stands for itself only after a backslash, as in \\(; or leave regex out to search for the text as written.`);
11131
- }
11132
- }
11133
- function searchProject(projectPath, options) {
11134
- const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
11135
- const result = {
11136
- query: options.query,
11137
- results: [],
11138
- summary: { files_searched: 0, files_with_matches: 0, total_matches: 0, truncated: false }
11139
- };
11140
- const regex = options.regex ? compiledQuery(options.query, options.caseSensitive) : null;
11141
- const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
11142
- const full = () => result.summary.truncated;
11143
- const visit = (directory) => {
11144
- if (steppedOver(directory)) {
11145
- return;
11146
- }
11147
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11148
- if (full()) {
11149
- return;
11150
- }
11151
- if (skipped(entry.name)) {
11152
- continue;
11153
- }
11154
- const entryPath = join14(directory, entry.name);
11155
- if (entry.isDirectory()) {
11156
- visit(entryPath);
11157
- continue;
11158
- }
11159
- const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
11160
- if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
11161
- continue;
11162
- }
11163
- const bytes = readFileSync9(entryPath);
11164
- if (extensions === null && isBinary(bytes)) {
11165
- continue;
11166
- }
11167
- result.summary.files_searched += 1;
11168
- const matches = [];
11169
- for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
11170
- const found = regex ? regex.exec(line) : plainMatch(line, needle, options.caseSensitive);
11171
- if (found === null) {
11172
- continue;
11173
- }
11174
- if (result.summary.total_matches >= options.maxResults) {
11175
- result.summary.truncated = true;
11176
- break;
11177
- }
11178
- matches.push({ line: index + 1, content: line.trim(), match: found[0] });
11179
- result.summary.total_matches += 1;
11180
- }
11181
- if (matches.length > 0) {
11182
- const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
11183
- result.results.push({ file: `res://${relativePath}`, matches });
11184
- result.summary.files_with_matches += 1;
11185
- }
11186
- }
11187
- };
11188
- visit(projectPath);
11189
- return result;
11190
- }
11191
- var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
11192
- var init_project_scan = __esm(() => {
11193
- init_errors();
11194
- SKIPPED = new Set(["node_modules"]);
11195
- SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
11196
- ASSET_EXTENSIONS = new Set([
11197
- "png",
11198
- "jpg",
11199
- "jpeg",
11200
- "webp",
11201
- "svg",
11202
- "bmp",
11203
- "tga",
11204
- "exr",
11205
- "hdr",
11206
- "ktx",
11207
- "dds",
11208
- "ttf",
11209
- "otf",
11210
- "woff",
11211
- "woff2",
11212
- "fnt",
11213
- "wav",
11214
- "mp3",
11215
- "ogg",
11216
- "glb",
11217
- "gltf",
11218
- "obj",
11219
- "fbx",
11220
- "blend",
11221
- "dae",
11222
- "ogv",
11223
- "gdshader",
11224
- "shader"
11225
- ]);
11226
- CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
11227
- BINARY_EXTENSIONS = new Set([
11228
- ...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
11229
- "scn",
11230
- "res",
11231
- "zip",
11232
- "pck",
11233
- "exe",
11234
- "dll",
11235
- "so",
11236
- "dylib"
11237
- ]);
11238
- });
11239
-
11240
11452
  // src/answer-json.ts
11241
11453
  function answerJson(value) {
11242
11454
  return JSON.stringify(value, null, 2).replace(SCALAR_ARRAY, (_whole, items) => `[${items.split(/,\n\s+/).join(", ")}]`);
@@ -11248,7 +11460,7 @@ var init_answer_json = __esm(() => {
11248
11460
  });
11249
11461
 
11250
11462
  // src/resources.ts
11251
- import { readFileSync as readFileSync10, realpathSync as realpathSync2 } from "node:fs";
11463
+ import { readFileSync as readFileSync11, realpathSync as realpathSync2 } from "node:fs";
11252
11464
  import { extname as extname2, resolve as resolve3 } from "node:path";
11253
11465
  function ensureProjectPath(getProjectPath) {
11254
11466
  const projectPath = getProjectPath();
@@ -11444,7 +11656,7 @@ function readResourceText(uri, getProjectPath) {
11444
11656
  const parsedUri = parseGodotUri(uri);
11445
11657
  if (parsedUri.kind === "project-info") {
11446
11658
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
11447
- const rawProject = readFileSync10(projectFilePath, "utf-8");
11659
+ const rawProject = readFileSync11(projectFilePath, "utf-8");
11448
11660
  const parsedProject = parseProjectGodot(rawProject);
11449
11661
  return {
11450
11662
  mimeType: "application/json",
@@ -11453,7 +11665,7 @@ function readResourceText(uri, getProjectPath) {
11453
11665
  }
11454
11666
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
11455
11667
  ensureAllowedExtension(parsedUri.kind, filePath);
11456
- const text = readFileSync10(filePath, "utf-8");
11668
+ const text = readFileSync11(filePath, "utf-8");
11457
11669
  return {
11458
11670
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
11459
11671
  text
@@ -11594,19 +11806,19 @@ function readParams(params, key) {
11594
11806
  }
11595
11807
 
11596
11808
  // src/setup.ts
11597
- import { createHash as createHash3 } from "node:crypto";
11809
+ import { createHash as createHash4 } from "node:crypto";
11598
11810
  import {
11599
11811
  cpSync,
11600
- existsSync as existsSync13,
11812
+ existsSync as existsSync14,
11601
11813
  mkdirSync as mkdirSync4,
11602
- readdirSync as readdirSync8,
11603
- readFileSync as readFileSync11,
11814
+ readdirSync as readdirSync9,
11815
+ readFileSync as readFileSync12,
11604
11816
  rmdirSync,
11605
11817
  rmSync as rmSync5,
11606
- statSync as statSync7,
11818
+ statSync as statSync8,
11607
11819
  writeFileSync as writeFileSync6
11608
11820
  } from "node:fs";
11609
- import { dirname as dirname8, join as join15 } from "node:path";
11821
+ import { dirname as dirname8, join as join16 } from "node:path";
11610
11822
  import { fileURLToPath } from "node:url";
11611
11823
  function editorAddonDigest(from = shippedAddonsDirectory()) {
11612
11824
  return addonsDigest(EDITOR_PLUGINS, from);
@@ -11615,27 +11827,27 @@ function runtimeAddonDigest(from = shippedAddonsDirectory()) {
11615
11827
  return addonsDigest([RUNTIME_ADDON], from);
11616
11828
  }
11617
11829
  function addonsDigest(names, from) {
11618
- const hash = createHash3("sha256");
11830
+ const hash = createHash4("sha256");
11619
11831
  const walk = (directory, relative) => {
11620
- for (const name of readdirSync8(directory).sort()) {
11832
+ for (const name of readdirSync9(directory).sort()) {
11621
11833
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
11622
11834
  continue;
11623
11835
  }
11624
- const path = join15(directory, name);
11836
+ const path = join16(directory, name);
11625
11837
  const inside = `${relative}/${name}`;
11626
- if (statSync7(path).isDirectory()) {
11838
+ if (statSync8(path).isDirectory()) {
11627
11839
  walk(path, inside);
11628
11840
  } else {
11629
11841
  hash.update(`${inside}
11630
11842
  `);
11631
- hash.update(readFileSync11(path));
11843
+ hash.update(readFileSync12(path));
11632
11844
  hash.update(`
11633
11845
  `);
11634
11846
  }
11635
11847
  }
11636
11848
  };
11637
11849
  for (const name of names) {
11638
- walk(join15(from, name), name);
11850
+ walk(join16(from, name), name);
11639
11851
  }
11640
11852
  return hash.digest("hex");
11641
11853
  }
@@ -11654,28 +11866,28 @@ function installedRuntimeDigest(projectPath) {
11654
11866
  return digestMarker(projectPath, RUNTIME_ADDON);
11655
11867
  }
11656
11868
  function digestMarker(projectPath, addon) {
11657
- const marker = join15(projectPath, "addons", addon, DIGEST_MARKER);
11658
- if (!existsSync13(marker)) {
11869
+ const marker = join16(projectPath, "addons", addon, DIGEST_MARKER);
11870
+ if (!existsSync14(marker)) {
11659
11871
  return null;
11660
11872
  }
11661
- const said = readFileSync11(marker, "utf8").trim();
11873
+ const said = readFileSync12(marker, "utf8").trim();
11662
11874
  return said === "" ? null : said;
11663
11875
  }
11664
11876
  function shippedAddonsDirectory() {
11665
- return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "addons");
11877
+ return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "addons");
11666
11878
  }
11667
11879
  function shippedOperationsScript() {
11668
- return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
11880
+ return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
11669
11881
  }
11670
11882
  function shippedListedRunner() {
11671
- return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "tests", "listed.gd");
11883
+ return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "tests", "listed.gd");
11672
11884
  }
11673
11885
  function gdUnit4Unusable(projectPath) {
11674
11886
  const marker = "addons/gdUnit4/plugin.cfg";
11675
- if (!existsSync13(join15(projectPath, ...marker.split("/")))) {
11887
+ if (!existsSync14(join16(projectPath, ...marker.split("/")))) {
11676
11888
  return { missing: true, reason: `gdUnit4 is not installed in this project: no ${marker}` };
11677
11889
  }
11678
- if (!existsSync13(join15(projectPath, ...GDUNIT4_CI_RUNNER.split("/")))) {
11890
+ if (!existsSync14(join16(projectPath, ...GDUNIT4_CI_RUNNER.split("/")))) {
11679
11891
  return {
11680
11892
  missing: false,
11681
11893
  reason: `gdUnit4 in this project is older than 6.0, which the shipped runner needs: no ${GDUNIT4_CI_RUNNER}`
@@ -11686,44 +11898,44 @@ function gdUnit4Unusable(projectPath) {
11686
11898
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
11687
11899
  const installed = [];
11688
11900
  for (const name of ADDONS) {
11689
- const source = join15(from, name);
11690
- if (!existsSync13(join15(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
11901
+ const source = join16(from, name);
11902
+ if (!existsSync14(join16(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
11691
11903
  throw new Error(`The package holds no ${name} addon at ${source}.`);
11692
11904
  }
11693
- const target = join15(projectPath, "addons", name);
11694
- const replaced = existsSync13(target);
11905
+ const target = join16(projectPath, "addons", name);
11906
+ const replaced = existsSync14(target);
11695
11907
  rmSync5(target, { recursive: true, force: true });
11696
11908
  mkdirSync4(dirname8(target), { recursive: true });
11697
11909
  cpSync(source, target, { recursive: true });
11698
- writeFileSync6(join15(target, VERSION_MARKER), `${SERVER_VERSION}
11910
+ writeFileSync6(join16(target, VERSION_MARKER), `${SERVER_VERSION}
11699
11911
  `);
11700
11912
  installed.push({ name, path: target, replaced });
11701
11913
  }
11702
- writeFileSync6(join15(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
11914
+ writeFileSync6(join16(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
11703
11915
  `);
11704
- writeFileSync6(join15(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
11916
+ writeFileSync6(join16(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
11705
11917
  `);
11706
11918
  return installed;
11707
11919
  }
11708
11920
  function installedAddonVersion(projectPath) {
11709
- const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
11710
- if (!existsSync13(marker)) {
11921
+ const marker = join16(projectPath, "addons", ADDONS[0], VERSION_MARKER);
11922
+ if (!existsSync14(marker)) {
11711
11923
  return null;
11712
11924
  }
11713
- const said = readFileSync11(marker, "utf8").trim();
11925
+ const said = readFileSync12(marker, "utf8").trim();
11714
11926
  return said === "" ? null : said;
11715
11927
  }
11716
11928
  function removeAddons(projectPath) {
11717
11929
  const removed = [];
11718
11930
  for (const name of ADDONS) {
11719
- const target = join15(projectPath, "addons", name);
11720
- if (existsSync13(target)) {
11931
+ const target = join16(projectPath, "addons", name);
11932
+ if (existsSync14(target)) {
11721
11933
  rmSync5(target, { recursive: true, force: true });
11722
11934
  removed.push(target);
11723
11935
  }
11724
11936
  }
11725
- const addons = join15(projectPath, "addons");
11726
- if (removed.length > 0 && existsSync13(addons) && readdirSync8(addons).length === 0) {
11937
+ const addons = join16(projectPath, "addons");
11938
+ if (removed.length > 0 && existsSync14(addons) && readdirSync9(addons).length === 0) {
11727
11939
  rmdirSync(addons);
11728
11940
  removed.push(addons);
11729
11941
  }
@@ -11757,7 +11969,7 @@ function loaderAmong(projectPath, named) {
11757
11969
  }
11758
11970
  let source = null;
11759
11971
  try {
11760
- source = readFileSync11(join15(projectPath, path), "utf8");
11972
+ source = readFileSync12(join16(projectPath, path), "utf8");
11761
11973
  } catch {}
11762
11974
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
11763
11975
  return { name: entry, path, exists: true };
@@ -11799,10 +12011,10 @@ function enabledPlugins(settings) {
11799
12011
  function inspectProject(projectPath) {
11800
12012
  const problems = [];
11801
12013
  const addons = ADDONS.map((name) => {
11802
- const directory = join15(projectPath, "addons", name);
11803
- const marker = join15(directory, VERSION_MARKER);
11804
- const installed = existsSync13(directory);
11805
- const version = existsSync13(marker) ? readFileSync11(marker, "utf8").trim() : null;
12014
+ const directory = join16(projectPath, "addons", name);
12015
+ const marker = join16(directory, VERSION_MARKER);
12016
+ const installed = existsSync14(directory);
12017
+ const version = existsSync14(marker) ? readFileSync12(marker, "utf8").trim() : null;
11806
12018
  const current = version === SERVER_VERSION;
11807
12019
  if (!installed) {
11808
12020
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -11811,7 +12023,7 @@ function inspectProject(projectPath) {
11811
12023
  }
11812
12024
  return { name, installed, version, current };
11813
12025
  });
11814
- const settings = parseProjectGodot(readFileSync11(join15(projectPath, "project.godot"), "utf8"));
12026
+ const settings = parseProjectGodot(readFileSync12(join16(projectPath, "project.godot"), "utf8"));
11815
12027
  const pluginsEnabled = enabledPlugins(settings);
11816
12028
  for (const name of EDITOR_PLUGINS) {
11817
12029
  if (!pluginsEnabled.includes(name)) {
@@ -11827,7 +12039,7 @@ function inspectProject(projectPath) {
11827
12039
  const tracked = trackedByGit(projectPath, [...named.values()]);
11828
12040
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
11829
12041
  for (const [name, path] of named) {
11830
- if (!existsSync13(join15(projectPath, path))) {
12042
+ if (!existsSync14(join16(projectPath, path))) {
11831
12043
  if (!addonMissing(path)) {
11832
12044
  problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
11833
12045
  }
@@ -11856,7 +12068,7 @@ function inspectProject(projectPath) {
11856
12068
  problems
11857
12069
  };
11858
12070
  }
11859
- var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", DIGEST_MARKER = ".gdharness-digest", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime, GDUNIT4_CI_RUNNER = "addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd";
12071
+ var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", QUIET_SCENES_SETTING = "gdharness/runtime/quiet_scenes_under", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", DIGEST_MARKER = ".gdharness-digest", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime, GDUNIT4_CI_RUNNER = "addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd";
11860
12072
  var init_setup = __esm(() => {
11861
12073
  init_class_cache();
11862
12074
  init_headless();
@@ -11980,6 +12192,7 @@ var init_tool_definitions = __esm(() => {
11980
12192
  init_launch();
11981
12193
  PROJECT_INFO_SECTIONS = [
11982
12194
  "autoloads",
12195
+ "classes",
11983
12196
  "input_actions",
11984
12197
  "plugins",
11985
12198
  "export_presets",
@@ -12147,13 +12360,13 @@ var init_tool_definitions = __esm(() => {
12147
12360
  name: "project_info",
12148
12361
  title: "Project info",
12149
12362
  effect: "read",
12150
- description: "What a project is: its name and main scene from project.godot, the Godot that answers, and how many scenes, scripts and assets it holds, other counting every remaining file, leaving out the addons gdharness installed and any folder holding a .gdignore, with optional sections on top.",
12363
+ description: "What a project is: its name and main scene from project.godot, the Godot that answers, and how many scenes, scripts and assets it holds, other counting every remaining file, leaving out the .import and .uid files the engine writes beside others, the addons gdharness installed and any folder holding a .gdignore, with optional sections on top.",
12151
12364
  parameters: {
12152
12365
  projectPath: PROJECT_PATH,
12153
12366
  include: {
12154
12367
  type: "array",
12155
12368
  items: { type: "string", enum: [...PROJECT_INFO_SECTIONS] },
12156
- description: "Extra sections: registered autoloads, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, and each file a scene, resource or script refers to that does not exist."
12369
+ description: "Extra sections: registered autoloads, the classes the project's scripts declare with class_name, each with its script, read from the files, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, and each file a scene, resource or script refers to that does not exist."
12157
12370
  },
12158
12371
  preset: { type: "string", description: "For validation: the export preset to validate against." },
12159
12372
  detail: {
@@ -12298,7 +12511,7 @@ var init_tool_definitions = __esm(() => {
12298
12511
  name: "project_search",
12299
12512
  title: "Search the project",
12300
12513
  effect: "read",
12301
- description: "Searches text or a regular expression across project files and returns file paths with line numbers. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
12514
+ description: "Searches text or a regular expression across project files and returns file paths with line numbers. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore, which not_searched names when the search passed one. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
12302
12515
  parameters: {
12303
12516
  projectPath: PROJECT_PATH,
12304
12517
  query: { type: "string", description: "The text or pattern to find." },
@@ -12324,7 +12537,7 @@ var init_tool_definitions = __esm(() => {
12324
12537
  direction: {
12325
12538
  type: "string",
12326
12539
  enum: ["forward", "reverse"],
12327
- description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
12540
+ description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
12328
12541
  },
12329
12542
  depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
12330
12543
  includeBuiltin: {
@@ -12354,7 +12567,7 @@ var init_tool_definitions = __esm(() => {
12354
12567
  options: {
12355
12568
  type: "object",
12356
12569
  ops: ["set_options"],
12357
- description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads. One already holding the value given is answered under unchanged_options rather than updated_options, and when none changed nothing is written or reimported.`,
12570
+ description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads. One already holding the value given is answered under unchanged_options rather than updated_options, and when none changed nothing is written or reimported. changed gives each option written as {was, now}, was null for one the import file did not hold.`,
12358
12571
  additionalProperties: true
12359
12572
  },
12360
12573
  reimport: {
@@ -12412,7 +12625,7 @@ var init_tool_definitions = __esm(() => {
12412
12625
  {
12413
12626
  name: "project_export",
12414
12627
  title: "Export",
12415
- description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without.",
12628
+ description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without. Its entries are the errors and warnings the export printed, the newest two hundred distinct ones, one printed more than once carrying times, and errors and warnings count them; exported is true only when this run wrote the output and printed no error.",
12416
12629
  parameters: {
12417
12630
  projectPath: PROJECT_PATH,
12418
12631
  preset: {
@@ -12448,19 +12661,19 @@ var init_tool_definitions = __esm(() => {
12448
12661
  name: "project_test",
12449
12662
  title: "Run tests",
12450
12663
  effect: "additive",
12451
- 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. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: 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. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. 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. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
12452
- answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for. " + IMPORTED_FIRST,
12664
+ 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. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: 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. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. 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. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
12665
+ answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw, and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, with the suite and the case that printed it, and declaredNotPrinted each declared error its case did not print. Either fails the run, and each suite holding one is listed with its count. engineErrors counts every engine error the run printed but those a suite declared. " + IMPORTED_FIRST,
12453
12666
  parameters: {
12454
12667
  projectPath: PROJECT_PATH,
12455
12668
  path: {
12456
12669
  type: ["string", "array"],
12457
12670
  items: { type: "string" },
12458
- description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. Default test."
12671
+ description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. Default tests and test, those the project has, which is where gdharness test looks."
12459
12672
  },
12460
12673
  ignore: {
12461
12674
  type: "array",
12462
12675
  items: { type: "string" },
12463
- description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out.'
12676
+ description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out. Every case the list left out comes back under ignored as suite_name:test_name, so a run called passed says what it did not run, and an entry naming no suite or case under path comes back under ignoreUnmatched.'
12464
12677
  },
12465
12678
  tests: {
12466
12679
  type: "array",
@@ -12696,7 +12909,7 @@ var init_tool_definitions = __esm(() => {
12696
12909
  {
12697
12910
  name: "scene_animation",
12698
12911
  title: "Animations",
12699
- description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. add_track on a path the animation already has a track of that type for adds the keys to that track, as the editor keys a property it already animates, answering existingTrack true and keysAdded beside keys, the track's count; a key at a time that track already has a key at is refused. A key past the animation's end, where it never plays, is written and named under pastLength. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.",
12912
+ description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. The player and the tree are nodes, so scene_node add puts one in the scene first. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. add_track on a path the animation already has a track of that type for adds the keys to that track, as the editor keys a property it already animates, answering existingTrack true and keysAdded beside keys, the track's count; a key at a time that track already has a key at is refused. A key past the animation's end, where it never plays, is written and named under pastLength. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.",
12700
12913
  parameters: {
12701
12914
  projectPath: PROJECT_PATH,
12702
12915
  scenePath: SCENE_PATH,
@@ -12904,7 +13117,7 @@ var init_tool_definitions = __esm(() => {
12904
13117
  operations: {
12905
13118
  structure: {
12906
13119
  effect: "read",
12907
- summary: "functions, variables (static ones marked is_static; one written with := has the type of a literal or built-in constructor it is given, marked inferred, as a constant always does), signals, constants, enums, inner_classes (each with its name, line and extends, and its own members in these same fields), class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies",
13120
+ summary: "functions, variables (static ones marked is_static; one written with := has the type of a literal or built-in constructor it is given, marked inferred, as a constant always does), signals, constants, enums, inner_classes (each with its name, line and extends, and its own members in these same fields), class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies. A member or inner class with a ## documentation comment directly above it carries the comment's text as doc, and the script's own, the ## block above class_name or extends or one followed by a blank line before any member, comes back as class_doc",
12908
13121
  requires: []
12909
13122
  },
12910
13123
  symbols: {
@@ -13042,7 +13255,7 @@ var init_tool_definitions = __esm(() => {
13042
13255
  name: "editor_launch",
13043
13256
  title: "Open, restart or close the editor, or bind its debug adapter again",
13044
13257
  description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; an editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. Godot tries each server port once, as the editor starts, so a restart binds a debug adapter that came back without its port again, in the editor that came back. rebind does the same for the connected editor without restarting it, whoever opened it; a debug session open on the adapter ends with it and is opened again by the next debug call. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so in editor_status rather than waiting for an editor that is not coming, and open finishes it. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
13045
- answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. restart answers with the version that came back, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up, and debugAdapterRebound says whether binding the debug adapter again took. serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
13258
+ answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. close and restart name under savedScenes the open scenes that had unsaved changes and were saved on the way out, an empty list when none had; a scene that would not save refuses the close or restart and leaves the editor running. Both answer tookMs, how long the editor took to go or to come back. restart answers with the version that came back, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up. When either came back without its port, serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind, and when it was the debug adapter, which is bound again in the editor that came back, debugAdapterRebound says whether that took; neither is there when both came up. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid, the lines that rebind made the panel print. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
13046
13259
  parameters: {
13047
13260
  projectPath: { ...PROJECT_PATH, ops: ["open"] },
13048
13261
  hidden: {
@@ -13595,7 +13808,11 @@ var init_tool_definitions = __esm(() => {
13595
13808
  },
13596
13809
  outputPath: {
13597
13810
  type: "string",
13598
- description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
13811
+ description: "Also save the picture as a lossless PNG at this absolute path, for somebody to open later or a script to compare; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
13812
+ },
13813
+ inline: {
13814
+ type: "boolean",
13815
+ description: "Send the picture back in the answer. Default true, as a JPEG when that is smaller than the PNG and the picture has no transparency (the text says so, with both sizes), since every picture an answer carries stays in the session for as long as it lasts. false sends none and needs outputPath, for a capture a script compares rather than one somebody looks at."
13599
13816
  }
13600
13817
  },
13601
13818
  requires: [],
@@ -13609,7 +13826,7 @@ var init_tool_definitions = __esm(() => {
13609
13826
  name: "runtime_input",
13610
13827
  title: "Send input to the game",
13611
13828
  description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
13612
- answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer. A mouse_click, mouse_motion or mouse_drag with a point outside the window's viewport says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
13829
+ answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer. An action or a key answers focused with the path of the Control holding the keyboard focus when it went in, since that Control gets it before the game's _unhandled_input: Space or Enter on a focused button presses the button. A mouse_click, mouse_motion or mouse_drag with a point outside the window's viewport says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
13613
13830
  parameters: {
13614
13831
  projectPath: RUNNING_PROJECT_PATH,
13615
13832
  pid: RUNNING_PID,
@@ -13897,7 +14114,7 @@ var init_tool_definitions = __esm(() => {
13897
14114
  operations: {
13898
14115
  pause: {
13899
14116
  effect: "additive",
13900
- summary: "stop a game this server started where it is, answering with the stack. The game takes it between frames, and in a script every 2048 lines it runs, so a loop that sleeps between few lines can take seconds; the call waits thirty, and past that answers paused false with the request still standing. Stopped between frames, where no script runs, the game is stepped on to the next line of script it runs",
14117
+ summary: "stop a game this server started where it is, answering with the stack. The game takes it between frames, and in a script every 2048 lines it runs, so a loop that sleeps between few lines can take seconds; the call waits thirty, and past that answers paused false with the request still standing. Stopped between frames, where no script runs, the game is stepped on to the next line of script it runs, and stopped in gdharness's runtime addon, an autoload whose _process runs first in every frame, on to the next line of the game's own script, for up to five seconds; one whose stack is still the addon's after them says so in a note",
13901
14118
  requires: []
13902
14119
  },
13903
14120
  continue: { effect: "additive", summary: "resume after a breakpoint", requires: [] },
@@ -14045,9 +14262,9 @@ function whatItCanChange(tool) {
14045
14262
  if (destroying.length === 0) {
14046
14263
  return "Adds or starts things, and overwrites and removes nothing itself.";
14047
14264
  }
14048
- return `Can overwrite, remove or end what is there, through ${listed(destroying)}.`;
14265
+ return `Can overwrite, remove or end what is there, through ${listed2(destroying)}.`;
14049
14266
  }
14050
- function listed(items) {
14267
+ function listed2(items) {
14051
14268
  const last = items.at(-1) ?? "";
14052
14269
  return items.length === 1 ? last : `${items.slice(0, -1).join(", ")} and ${last}`;
14053
14270
  }
@@ -14105,30 +14322,30 @@ var init_tool_reference = __esm(() => {
14105
14322
  });
14106
14323
 
14107
14324
  // src/desktop.ts
14108
- import { createHash as createHash4 } from "node:crypto";
14325
+ import { createHash as createHash5 } from "node:crypto";
14109
14326
  import {
14110
14327
  closeSync,
14111
- existsSync as existsSync15,
14328
+ existsSync as existsSync16,
14112
14329
  mkdtempSync as mkdtempSync2,
14113
14330
  openSync,
14114
- readFileSync as readFileSync12,
14331
+ readFileSync as readFileSync13,
14115
14332
  rmSync as rmSync7,
14116
- statSync as statSync8,
14333
+ statSync as statSync9,
14117
14334
  writeFileSync as writeFileSync8,
14118
14335
  writeSync
14119
14336
  } from "node:fs";
14120
14337
  import { tmpdir as tmpdir4 } from "node:os";
14121
- import { join as join17 } from "node:path";
14338
+ import { join as join18 } from "node:path";
14122
14339
  import { setTimeout as delay } from "node:timers/promises";
14123
14340
  function helperAssembly(directory = tmpdir4()) {
14124
- return join17(directory, `gdharness-desktop-${DIGEST}.dll`);
14341
+ return join18(directory, `gdharness-desktop-${DIGEST}.dll`);
14125
14342
  }
14126
14343
  async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COMPILE_MS, wait = true) {
14127
14344
  const assembly = helperAssembly(directory);
14128
14345
  const lock = `${assembly}.compiling`;
14129
14346
  const until = Date.now() + budgetMs;
14130
14347
  while (Date.now() < until) {
14131
- if (existsSync15(assembly)) {
14348
+ if (existsSync16(assembly)) {
14132
14349
  return null;
14133
14350
  }
14134
14351
  let mine = false;
@@ -14146,8 +14363,8 @@ async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COM
14146
14363
  }
14147
14364
  }
14148
14365
  try {
14149
- const holder = Number(readFileSync12(lock, "utf8").trim());
14150
- const old = Date.now() - statSync8(lock).mtimeMs > budgetMs;
14366
+ const holder = Number(readFileSync13(lock, "utf8").trim());
14367
+ const old = Date.now() - statSync9(lock).mtimeMs > budgetMs;
14151
14368
  if (old || Number.isInteger(holder) && holder > 0 && !alive(holder)) {
14152
14369
  rmSync7(lock, { force: true });
14153
14370
  continue;
@@ -14161,8 +14378,8 @@ async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COM
14161
14378
  return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
14162
14379
  }
14163
14380
  async function compileHelper(directory, assembly, budgetMs) {
14164
- const scratch = mkdtempSync2(join17(directory, "gdharness-desktop-source-"));
14165
- const source = join17(scratch, "helper.cs");
14381
+ const scratch = mkdtempSync2(join18(directory, "gdharness-desktop-source-"));
14382
+ const source = join18(scratch, "helper.cs");
14166
14383
  writeFileSync8(source, SOURCE, "utf8");
14167
14384
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
14168
14385
  const script = [
@@ -14200,7 +14417,7 @@ async function compileHelper(directory, assembly, budgetMs) {
14200
14417
  });
14201
14418
  compiler.once("close", (code) => {
14202
14419
  clearTimeout(timer);
14203
- resolve(existsSync15(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
14420
+ resolve(existsSync16(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
14204
14421
  });
14205
14422
  });
14206
14423
  } finally {
@@ -14425,7 +14642,317 @@ public static class GdharnessDesktop {
14425
14642
  var init_desktop = __esm(() => {
14426
14643
  init_alive();
14427
14644
  init_spawn();
14428
- DIGEST = createHash4("sha256").update(SOURCE).digest("hex").slice(0, 16);
14645
+ DIGEST = createHash5("sha256").update(SOURCE).digest("hex").slice(0, 16);
14646
+ });
14647
+
14648
+ // src/suite-judge.ts
14649
+ import { readFileSync as readFileSync16 } from "node:fs";
14650
+ import { stripVTControlCharacters } from "node:util";
14651
+ function secondsOf(time) {
14652
+ let ms = 0;
14653
+ for (const [, amount, unit] of time.matchAll(/(\d+)(h|min|ms|s)\b/g)) {
14654
+ ms += Number(amount) * (unit === "h" ? 3600000 : unit === "min" ? 60000 : unit === "s" ? 1000 : 1);
14655
+ }
14656
+ return ms / 1000;
14657
+ }
14658
+ function funcOf(name) {
14659
+ const colon = name.indexOf(":");
14660
+ return colon < 0 ? name : name.slice(0, colon);
14661
+ }
14662
+ function readRun(printed) {
14663
+ const lines = stripVTControlCharacters(printed).replaceAll(`\r
14664
+ `, `
14665
+ `).split(`
14666
+ `);
14667
+ const suites = new Map;
14668
+ const errors = [];
14669
+ const leaks = [];
14670
+ let executed = null;
14671
+ let open = null;
14672
+ let during = null;
14673
+ let afterCase = [];
14674
+ let lastCase = null;
14675
+ const settleAfterCase = (owner) => {
14676
+ for (const held of afterCase) {
14677
+ errors.push({ ...held, suite: open?.path ?? null, during: owner });
14678
+ }
14679
+ afterCase = [];
14680
+ };
14681
+ for (const [at, line] of lines.entries()) {
14682
+ const opened = SUITE_OPENED.exec(line);
14683
+ if (opened?.[1] !== undefined) {
14684
+ settleAfterCase("after");
14685
+ open = suites.get(opened[1]) ?? { path: opened[1], cases: [], closed: null };
14686
+ suites.set(open.path, open);
14687
+ during = "before";
14688
+ lastCase = null;
14689
+ continue;
14690
+ }
14691
+ const caseLine = CASE_LINE.exec(line);
14692
+ if (caseLine !== null && open !== null) {
14693
+ const name = caseLine[2] ?? "";
14694
+ if (caseLine[3] === "STARTED") {
14695
+ settleAfterCase(lastCase ?? "before");
14696
+ during = name;
14697
+ } else {
14698
+ open.cases.push({
14699
+ name,
14700
+ func: funcOf(name),
14701
+ verdict: caseLine[3] ?? "",
14702
+ seconds: secondsOf(caseLine[4] ?? ""),
14703
+ at
14704
+ });
14705
+ lastCase = name;
14706
+ during = null;
14707
+ }
14708
+ continue;
14709
+ }
14710
+ const statistics = STATISTICS.exec(line);
14711
+ if (statistics !== null && open !== null) {
14712
+ settleAfterCase("after");
14713
+ open.closed = { verdict: statistics[1] ?? "", seconds: secondsOf(statistics[2] ?? "") };
14714
+ open = null;
14715
+ during = null;
14716
+ lastCase = null;
14717
+ continue;
14718
+ }
14719
+ const counted = EXECUTED.exec(line);
14720
+ if (counted?.[1] !== undefined) {
14721
+ executed = Number(counted[1]);
14722
+ continue;
14723
+ }
14724
+ if (LEAK.test(line)) {
14725
+ leaks.push(line.trim());
14726
+ continue;
14727
+ }
14728
+ if (suites.size === 0 && DRIVER_FALLBACK.test(line)) {
14729
+ errors.length = 0;
14730
+ continue;
14731
+ }
14732
+ const error = ERROR_LINE.exec(line);
14733
+ if (error === null) {
14734
+ continue;
14735
+ }
14736
+ const text = error[1] ?? "";
14737
+ if (open === null) {
14738
+ errors.push({ text, at, suite: null, during: null });
14739
+ } else if (during === null) {
14740
+ afterCase.push({ text, at });
14741
+ } else {
14742
+ errors.push({ text, at, suite: open.path, during });
14743
+ }
14744
+ }
14745
+ const last = open;
14746
+ const stoppedIn = last === null ? null : during === null ? `${last.path}, after ${lastCase ?? "its cases"}` : during === "before" ? `${last.path}, in before` : `${last.path} > ${during}`;
14747
+ settleAfterCase("after");
14748
+ const joined = lines.join(`
14749
+ `);
14750
+ const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
14751
+ const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
14752
+ return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn };
14753
+ }
14754
+ function readExpected(file) {
14755
+ try {
14756
+ const parsed = JSON.parse(readFileSync16(file, "utf8"));
14757
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
14758
+ } catch {
14759
+ return {};
14760
+ }
14761
+ }
14762
+ function exitNamed(code) {
14763
+ const unsigned = code >>> 0;
14764
+ const status = WINDOWS_EXITS.get(unsigned);
14765
+ if (status !== undefined) {
14766
+ return `0x${unsigned.toString(16).toUpperCase()} (${status})`;
14767
+ }
14768
+ const gdunit = GDUNIT_EXITS.get(code);
14769
+ return gdunit === undefined ? String(code) : `${String(code)} (${gdunit})`;
14770
+ }
14771
+ function named(items) {
14772
+ const shown = items.slice(0, MOST_NAMED).join(", ");
14773
+ return items.length > MOST_NAMED ? `${shown} and ${String(items.length - MOST_NAMED)} more` : shown;
14774
+ }
14775
+ function judgeEngine(printed, handed, end) {
14776
+ const faults = [];
14777
+ const failing = new Set;
14778
+ const shownAt = new Set;
14779
+ const explained = end.code === GDUNIT_FAILED && [...printed.suites.values()].some((suite) => suite.cases.some((ran) => ran.verdict === "FAILED"));
14780
+ if (end.ended !== null) {
14781
+ faults.push(end.ended);
14782
+ } else if (end.code !== 0 && !explained) {
14783
+ faults.push(end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
14784
+ }
14785
+ if (printed.unloadable.length > 0) {
14786
+ faults.push(`gdUnit4 could not load ${named(printed.unloadable)}, so it ran nothing`);
14787
+ }
14788
+ if (printed.executed === null || printed.executed === 0) {
14789
+ faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
14790
+ }
14791
+ for (const { path, cases } of handed.suites) {
14792
+ const suite = printed.suites.get(path);
14793
+ if (suite === undefined) {
14794
+ faults.push(`${path} never started`);
14795
+ failing.add(path);
14796
+ continue;
14797
+ }
14798
+ if (suite.closed === null) {
14799
+ faults.push(`${path} never finished`);
14800
+ failing.add(path);
14801
+ } else if (suite.closed.verdict !== "PASSED") {
14802
+ if (suite.cases.every((ran) => ran.verdict !== "FAILED")) {
14803
+ faults.push(`${path} reported ${suite.closed.verdict} with every case passing, so a hook failed`);
14804
+ }
14805
+ failing.add(path);
14806
+ }
14807
+ const finished = new Set(suite.cases.map((ran) => ran.func));
14808
+ const missing = cases.filter((func) => !finished.has(func));
14809
+ if (missing.length > 0 && suite.closed !== null) {
14810
+ faults.push(`${path} closed without running ${named(missing)}`);
14811
+ failing.add(path);
14812
+ }
14813
+ const wanted = new Set(cases);
14814
+ const unasked = [...finished].filter((func) => !wanted.has(func));
14815
+ if (unasked.length > 0) {
14816
+ faults.push(`${path} ran ${named(unasked)}, which gdharness did not read as cases of it to run here, so its ` + "plan of the cases was wrong");
14817
+ failing.add(path);
14818
+ }
14819
+ for (const ran of suite.cases) {
14820
+ if (ran.verdict === "FAILED" || ran.verdict === "FLAKY") {
14821
+ faults.push(ran.verdict === "FLAKY" ? `${path} > ${ran.name} failed before it passed on a retry` : `${path} > ${ran.name} failed`);
14822
+ failing.add(path);
14823
+ for (let at = ran.at;at < printed.lines.length && (at === ran.at || printed.lines[at]?.trim()); at += 1) {
14824
+ shownAt.add(at);
14825
+ }
14826
+ }
14827
+ }
14828
+ }
14829
+ const unexpected = unexpectedErrors(printed, handed.expected, handed.suites.map((suite) => suite.path));
14830
+ for (const error of unexpected.printed) {
14831
+ faults.push(error.suite === null ? `an engine error outside every suite: ${error.text}` : `an engine error in ${error.suite} > ${error.during ?? ""}: ${error.text}; ${declaringAdvice(error.during ?? "")}`);
14832
+ if (error.suite !== null) {
14833
+ failing.add(error.suite);
14834
+ }
14835
+ shownAt.add(error.at);
14836
+ shownAt.add(error.at + 1);
14837
+ }
14838
+ for (const missing of unexpected.neverPrinted) {
14839
+ faults.push(`${missing.suite} > ${missing.during} declares the error "${missing.text}", which it never printed`);
14840
+ failing.add(missing.suite);
14841
+ }
14842
+ for (const leak of printed.leaks) {
14843
+ faults.push(`the engine exited holding what it should have freed: ${leak}`);
14844
+ }
14845
+ const memoryExit = end.code !== null && MEMORY_EXITS.has(end.code >>> 0);
14846
+ const starved = memoryExit || printed.starved.length > 0;
14847
+ if (printed.starved.length > 0) {
14848
+ faults.push("the engine ran out of memory");
14849
+ }
14850
+ printed.lines.forEach((line, at) => {
14851
+ if (/^(?:Overall Summary|Executed test|Total execution time| ?Exit code)/.test(line.trim())) {
14852
+ shownAt.add(at);
14853
+ }
14854
+ });
14855
+ const passed = faults.length === 0;
14856
+ const passedSuites = handed.suites.map((suite) => suite.path).filter((path) => !failing.has(path));
14857
+ const cutShort = end.ended !== null || printed.executed === null || handed.suites.some(({ path }) => printed.suites.get(path)?.closed == null);
14858
+ return {
14859
+ passed,
14860
+ faults,
14861
+ passedSuites,
14862
+ starved,
14863
+ shown: passed ? [] : linesShown(printed.lines, shownAt, cutShort)
14864
+ };
14865
+ }
14866
+ function linesShown(lines, picked, cutShort) {
14867
+ const summary = [...picked].filter((at) => at < lines.length).sort((a, b) => a - b);
14868
+ const telling = summary.some((at) => !/^(?:Overall Summary|Executed test|Total execution|\s*Exit code)/.test(lines[at]?.trim() ?? ""));
14869
+ if (!telling && !cutShort) {
14870
+ return [];
14871
+ }
14872
+ if (!telling) {
14873
+ return lines.slice(Math.max(0, lines.length - TAIL)).filter((line, at, all) => at < all.length - 1 || line !== "");
14874
+ }
14875
+ const shown = [];
14876
+ let previous = -1;
14877
+ for (const at of summary) {
14878
+ if (previous >= 0 && at > previous + 1) {
14879
+ shown.push("...");
14880
+ }
14881
+ shown.push(lines[at] ?? "");
14882
+ previous = at;
14883
+ }
14884
+ return shown;
14885
+ }
14886
+ function declaringAdvice(during) {
14887
+ return `if the suite prints it on purpose, list its text under "${funcOf(during)}" in the suite's GDHARNESS_EXPECTED_ERRORS`;
14888
+ }
14889
+ function unexpectedErrors(printed, expected, paths) {
14890
+ const owed = new Map;
14891
+ for (const path of paths) {
14892
+ const suite = printed.suites.get(path);
14893
+ const declared = expected[path];
14894
+ if (suite === undefined || declared === undefined) {
14895
+ continue;
14896
+ }
14897
+ const ranHooks = suite.closed === null ? ["before"] : ["before", "after"];
14898
+ for (const during of [...ranHooks, ...suite.cases.map((ran) => ran.name)]) {
14899
+ const texts = declared[funcOf(during)];
14900
+ if (Array.isArray(texts) && texts.length > 0) {
14901
+ owed.set(`${path}
14902
+ ${during}`, {
14903
+ suite: path,
14904
+ during,
14905
+ texts: texts.map((text) => String(text))
14906
+ });
14907
+ }
14908
+ }
14909
+ }
14910
+ const unexpected = [];
14911
+ for (const error of printed.errors) {
14912
+ const debt = error.suite === null || error.during === null ? undefined : owed.get(`${error.suite}
14913
+ ${error.during}`);
14914
+ const index = debt?.texts.indexOf(error.text) ?? -1;
14915
+ if (debt === undefined || index < 0) {
14916
+ unexpected.push(error);
14917
+ } else {
14918
+ debt.texts.splice(index, 1);
14919
+ }
14920
+ }
14921
+ const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text })));
14922
+ return { printed: unexpected, neverPrinted };
14923
+ }
14924
+ var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, EXECUTED, DRIVER_FALLBACK, LEAK, STARVED, WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, TAIL = 60, MOST_NAMED = 8;
14925
+ var init_suite_judge = __esm(() => {
14926
+ init_junit();
14927
+ SUITE_OPENED = /^Run Test Suite:\s*(res:\/\/.+?\.gd)\s*$/;
14928
+ TIME = String.raw`(?:\d+h )?(?:\d+min )?(?:\d+s )?\d+ms`;
14929
+ CASE_LINE = new RegExp(String.raw`^\s*(res://.+?\.gd) > (.+?) (STARTED|PASSED|FAILED|FLAKY|SKIPPED|WARNING)(?: \((?:retry \d+|\d+ retries)\))?(?: (${TIME}))?\s*$`);
14930
+ STATISTICS = new RegExp(String.raw`^Statistics:.*\|\s*([A-Z]+)(?:\s+\(.*?\))?\s+(${TIME})\s*$`);
14931
+ ERROR_LINE = /^\s*(?:SCRIPT |USER )?ERROR: (.*?)\s*$/;
14932
+ EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
14933
+ DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
14934
+ LEAK = /ObjectDB instances (?:were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit/;
14935
+ STARVED = [
14936
+ /Parameter "mem(?:_new)?" is null\.?\s*\n\s*at: \w+ \((?:\.\/)?core\/(?:os\/memory\.cpp|templates\/cowdata\.h):/g,
14937
+ /ERROR: Failed decoding WebP image\.\s*\n\s*at: _webp_unpack/g
14938
+ ];
14939
+ WINDOWS_EXITS = new Map([
14940
+ [3221225477, "STATUS_ACCESS_VIOLATION"],
14941
+ [3221225495, "STATUS_NO_MEMORY"],
14942
+ [3221225773, "STATUS_COMMITMENT_LIMIT"],
14943
+ [3221225794, "STATUS_DLL_INIT_FAILED"],
14944
+ [3221226505, "STATUS_STACK_BUFFER_OVERRUN"],
14945
+ [3228369022, "a module that could not be found (VcppException)"],
14946
+ [3228369023, "a procedure that could not be found (VcppException)"]
14947
+ ]);
14948
+ MEMORY_EXITS = new Set([3221225495, 3221225773]);
14949
+ GDUNIT_EXITS = new Map([
14950
+ [GDUNIT_FAILED, "gdUnit4's code for a case that failed or errored"],
14951
+ [101, "gdUnit4's code for orphan nodes left behind"],
14952
+ [103, "gdUnit4's code for a suite that cannot run headless"],
14953
+ [104, "gdUnit4's code for a Godot version it does not support"],
14954
+ [105, "gdUnit4's code for scripts that did not compile"]
14955
+ ]);
14429
14956
  });
14430
14957
 
14431
14958
  // src/whole-file.ts
@@ -14459,6 +14986,235 @@ var init_whole_file = __esm(() => {
14459
14986
  HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
14460
14987
  });
14461
14988
 
14989
+ // src/suite-plan.ts
14990
+ import { existsSync as existsSync18, mkdirSync as mkdirSync7, readFileSync as readFileSync17 } from "node:fs";
14991
+ import { dirname as dirname10, posix as posix3 } from "node:path";
14992
+
14993
+ class ProjectScripts {
14994
+ sources = new Map;
14995
+ classes = new Map;
14996
+ read = new Map;
14997
+ projectPath;
14998
+ constructor(projectPath) {
14999
+ this.projectPath = projectPath;
15000
+ eachScript(projectPath, (script, source) => {
15001
+ this.sources.set(script, source);
15002
+ const declared = declaredClassOf(source);
15003
+ if (declared !== null) {
15004
+ this.classes.set(declared, script);
15005
+ }
15006
+ });
15007
+ }
15008
+ script(path) {
15009
+ const known = this.read.get(path);
15010
+ if (known !== undefined) {
15011
+ return known;
15012
+ }
15013
+ const source = this.sources.get(path);
15014
+ if (source === undefined) {
15015
+ return;
15016
+ }
15017
+ const shape = shapeOf(source);
15018
+ const read = {
15019
+ source,
15020
+ extendsAs: shape.body.extendsAs,
15021
+ funcs: shape.body.declarations.filter((d) => d.kind === "func").map((d) => d.name),
15022
+ code: codeOf(source)
15023
+ };
15024
+ this.read.set(path, read);
15025
+ return read;
15026
+ }
15027
+ base(path) {
15028
+ const extendsAs = this.script(path)?.extendsAs ?? null;
15029
+ if (extendsAs === null) {
15030
+ return null;
15031
+ }
15032
+ const quote = extendsAs[0];
15033
+ if (quote === '"' || quote === "'") {
15034
+ const target = extendsAs.slice(1, -1);
15035
+ return {
15036
+ path: target.startsWith("res://") ? target : `res://${posix3.join(posix3.dirname(path.slice(6)), target)}`
15037
+ };
15038
+ }
15039
+ const name = extendsAs.split(".")[0] ?? extendsAs;
15040
+ const declaring = this.classes.get(name);
15041
+ return declaring === undefined ? { name } : { path: declaring };
15042
+ }
15043
+ suite(path) {
15044
+ const cases = new Set;
15045
+ let windowed = false;
15046
+ const seen = new Set;
15047
+ let at = path;
15048
+ while (!seen.has(at)) {
15049
+ seen.add(at);
15050
+ const read = this.script(at);
15051
+ if (read === undefined) {
15052
+ return null;
15053
+ }
15054
+ for (const func of read.funcs.filter((name) => name.startsWith(CASE_PREFIX))) {
15055
+ cases.add(func);
15056
+ }
15057
+ windowed ||= NEEDS_WINDOW.test(read.code) || SCENE_RUNNER.test(read.code);
15058
+ const base = this.base(at);
15059
+ if (base === null) {
15060
+ return null;
15061
+ }
15062
+ if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
15063
+ return cases.size > 0 ? { path, cases: [...cases].sort(), windowed } : null;
15064
+ }
15065
+ if ("name" in base) {
15066
+ return null;
15067
+ }
15068
+ at = base.path;
15069
+ }
15070
+ return null;
15071
+ }
15072
+ suitesUnder(roots) {
15073
+ const prefixes = roots.map((root) => root.endsWith("/") ? root : `${root}/`);
15074
+ return [...this.sources.keys()].filter((path) => prefixes.some((prefix) => path.startsWith(prefix))).sort().flatMap((path) => this.suite(path) ?? []);
15075
+ }
15076
+ }
15077
+ function readTimes(file) {
15078
+ try {
15079
+ const parsed = JSON.parse(readFileSync17(file, "utf8"));
15080
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
15081
+ } catch {
15082
+ return {};
15083
+ }
15084
+ }
15085
+ function writeJson(file, value) {
15086
+ mkdirSync7(dirname10(file), { recursive: true });
15087
+ writeWhole(file, `${JSON.stringify(value, null, 1)}
15088
+ `);
15089
+ }
15090
+ function timesAfter(times, suites, runs) {
15091
+ const next = { ...times };
15092
+ const declared = new Map(suites.map((suite) => [suite.path, suite]));
15093
+ const measured = new Map;
15094
+ for (const run of runs) {
15095
+ for (const printed of run.suites.values()) {
15096
+ if (printed.closed === null || !declared.has(printed.path)) {
15097
+ continue;
15098
+ }
15099
+ const entry = measured.get(printed.path) ?? { cases: new Map, around: 0 };
15100
+ const own = new Map;
15101
+ for (const ran of printed.cases) {
15102
+ own.set(ran.func, (own.get(ran.func) ?? 0) + ran.seconds);
15103
+ }
15104
+ const inCases = [...own.values()].reduce((sum, seconds) => sum + seconds, 0);
15105
+ entry.around = Math.max(entry.around, printed.closed.seconds - inCases);
15106
+ for (const [func, seconds] of own) {
15107
+ entry.cases.set(func, seconds);
15108
+ }
15109
+ measured.set(printed.path, entry);
15110
+ }
15111
+ }
15112
+ for (const [path, entry] of measured) {
15113
+ const suite = declared.get(path);
15114
+ const cases = {};
15115
+ for (const func of suite?.cases ?? []) {
15116
+ const seconds = entry.cases.get(func) ?? times[path]?.cases[func];
15117
+ if (seconds !== undefined) {
15118
+ cases[func] = round(seconds);
15119
+ }
15120
+ }
15121
+ const inCases = Object.values(cases).reduce((sum, seconds) => sum + seconds, 0);
15122
+ next[path] = { seconds: round(entry.around + inCases), cases };
15123
+ }
15124
+ return next;
15125
+ }
15126
+ function round(seconds) {
15127
+ return Math.round(seconds * 1000) / 1000;
15128
+ }
15129
+ function median(values) {
15130
+ if (values.length === 0) {
15131
+ return;
15132
+ }
15133
+ const sorted = [...values].sort((a, b) => a - b);
15134
+ const middle = Math.floor(sorted.length / 2);
15135
+ return sorted.length % 2 === 1 ? sorted[middle] : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
15136
+ }
15137
+ function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
15138
+ const known = Object.values(times);
15139
+ const caseCost = median(known.flatMap((time) => Object.values(time.cases))) ?? UNTIMED_CASE;
15140
+ const aroundCost = median(known.map((time) => time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0))) ?? 0;
15141
+ return suites.flatMap((suite) => {
15142
+ const time = times[suite.path];
15143
+ const costOf = (func) => time?.cases[func] ?? caseCost;
15144
+ const around = time === undefined ? aroundCost : Math.max(0, time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0));
15145
+ const whole = around + suite.cases.reduce((sum, func) => sum + costOf(func), 0);
15146
+ if (suite.windowed || whole <= splitPast || suite.cases.length < 2) {
15147
+ return [{ suite: suite.path, cases: suite.cases, cost: whole, windowed: suite.windowed }];
15148
+ }
15149
+ return suite.cases.map((func) => ({
15150
+ suite: suite.path,
15151
+ cases: [func],
15152
+ cost: around + costOf(func),
15153
+ windowed: false
15154
+ }));
15155
+ });
15156
+ }
15157
+ function dealt(units, declared, most) {
15158
+ const windowed = units.filter((unit) => unit.windowed);
15159
+ const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? ""));
15160
+ if (units.length === 0) {
15161
+ return [];
15162
+ }
15163
+ const together = windowed.reduce((sum, unit) => sum + unit.cost, 0);
15164
+ const total = units.reduce((sum, unit) => sum + unit.cost, 0);
15165
+ const slowest = Math.max(together, headless[0]?.cost ?? 0);
15166
+ const count = Math.max(1, Math.min(most, headless.length + (windowed.length > 0 ? 1 : 0), Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
15167
+ const bins = Array.from({ length: count }, (_, index) => index === 0 ? [...windowed] : []);
15168
+ const loads = Array.from({ length: count }, (_, index) => index === 0 ? together : 0);
15169
+ for (const unit of headless) {
15170
+ const lightest = loads.indexOf(Math.min(...loads));
15171
+ bins[lightest]?.push(unit);
15172
+ loads[lightest] = (loads[lightest] ?? 0) + unit.cost;
15173
+ }
15174
+ return bins.map((bin, index) => planOf(bin, declared, index === 0 && windowed.length > 0));
15175
+ }
15176
+ function windowedSaid(plans) {
15177
+ if (!plans.some((plan) => plan.windowed)) {
15178
+ return "";
15179
+ }
15180
+ return plans.length === 1 ? ", with a window" : ", one of them windowed";
15181
+ }
15182
+ function inOneEngine(units, declared) {
15183
+ return planOf(units, declared, units.some((unit) => unit.windowed));
15184
+ }
15185
+ function planOf(bin, declared, windowed) {
15186
+ const held = new Map;
15187
+ for (const unit of bin) {
15188
+ held.set(unit.suite, [...held.get(unit.suite) ?? [], ...unit.cases]);
15189
+ }
15190
+ const skipping = [...held].flatMap(([path, cases]) => (declared.get(path) ?? []).filter((func) => !cases.includes(func)).map((func) => `${path}:${func}`));
15191
+ return {
15192
+ suites: [...held].map(([path, cases]) => ({ path, cases: [...cases].sort() })),
15193
+ skipping,
15194
+ cost: bin.reduce((sum, unit) => sum + unit.cost, 0),
15195
+ windowed
15196
+ };
15197
+ }
15198
+ function viewportOf(projectPath) {
15199
+ const file = `${projectPath}/project.godot`;
15200
+ if (!existsSync18(file)) {
15201
+ return null;
15202
+ }
15203
+ const text = readFileSync17(file, "utf8");
15204
+ const width = /^window\/size\/viewport_width\s*=\s*(\d+)/m.exec(text)?.[1];
15205
+ const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
15206
+ return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
15207
+ }
15208
+ var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5;
15209
+ var init_suite_plan = __esm(() => {
15210
+ init_class_cache();
15211
+ init_gdscript_source();
15212
+ init_whole_file();
15213
+ NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*true\b/m;
15214
+ TEST_DIRECTORIES = ["tests", "test"];
15215
+ SCENE_RUNNER = /\bscene_runner\s*\(/;
15216
+ });
15217
+
14462
15218
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
14463
15219
  var __create2, __defProp2, __getOwnPropDesc, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __exportAll = (all, symbols) => {
14464
15220
  let target = {};
@@ -36060,13 +36816,13 @@ var init_stdio = __esm(() => {
36060
36816
  });
36061
36817
 
36062
36818
  // src/autoload-failure.ts
36063
- import { existsSync as existsSync19, readFileSync as readFileSync18 } from "node:fs";
36064
- import { join as join22 } from "node:path";
36819
+ import { existsSync as existsSync20, readFileSync as readFileSync20 } from "node:fs";
36820
+ import { join as join23 } from "node:path";
36065
36821
  function scriptsAutoloaded(projectPath) {
36066
36822
  const scripts = new Map;
36067
36823
  let settings;
36068
36824
  try {
36069
- settings = parseProjectGodot(readFileSync18(join22(projectPath, "project.godot"), "utf8"));
36825
+ settings = parseProjectGodot(readFileSync20(join23(projectPath, "project.godot"), "utf8"));
36070
36826
  } catch {
36071
36827
  return scripts;
36072
36828
  }
@@ -36075,12 +36831,12 @@ function scriptsAutoloaded(projectPath) {
36075
36831
  scripts.set(`res://${path}`, name);
36076
36832
  continue;
36077
36833
  }
36078
- const scene = join22(projectPath, path);
36079
- if (!existsSync19(scene)) {
36834
+ const scene = join23(projectPath, path);
36835
+ if (!existsSync20(scene)) {
36080
36836
  continue;
36081
36837
  }
36082
36838
  try {
36083
- for (const named of readFileSync18(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
36839
+ for (const named of readFileSync20(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
36084
36840
  scripts.set(named[1] ?? "", name);
36085
36841
  }
36086
36842
  } catch {}
@@ -36099,10 +36855,10 @@ var init_autoload_failure = __esm(() => {
36099
36855
  });
36100
36856
 
36101
36857
  // src/runtime-client.ts
36102
- import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync19, statSync as statSync12, unlinkSync as unlinkSync2 } from "node:fs";
36858
+ import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync21, statSync as statSync13, unlinkSync as unlinkSync2 } from "node:fs";
36103
36859
  import { createConnection } from "node:net";
36104
36860
  import { tmpdir as tmpdir5 } from "node:os";
36105
- import { dirname as dirname12, join as join23, resolve as resolve5 } from "node:path";
36861
+ import { dirname as dirname12, join as join24, resolve as resolve5 } from "node:path";
36106
36862
  import { setTimeout as delay4 } from "node:timers/promises";
36107
36863
  function runtimeDirectory(variables = process.env) {
36108
36864
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -36110,20 +36866,20 @@ function runtimeDirectory(variables = process.env) {
36110
36866
  return explicit;
36111
36867
  }
36112
36868
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
36113
- return join23(perUser ?? tmpdir5(), "gdharness");
36869
+ return join24(perUser ?? tmpdir5(), "gdharness");
36114
36870
  }
36115
36871
  function runtimeDirectories(variables = process.env) {
36116
36872
  const candidates = [runtimeDirectory(variables)];
36117
36873
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
36118
36874
  for (const base of fallbacks) {
36119
- candidates.push(join23(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36875
+ candidates.push(join24(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36120
36876
  }
36121
36877
  return [...new Set(candidates.map((path) => resolve5(path)))];
36122
36878
  }
36123
36879
  function errorReportOf(pid, directories = runtimeDirectories()) {
36124
36880
  for (const directory of directories) {
36125
- const path = join23(directory, `runtime-${pid}.log`);
36126
- if (existsSync20(path)) {
36881
+ const path = join24(directory, `runtime-${pid}.log`);
36882
+ if (existsSync21(path)) {
36127
36883
  return path;
36128
36884
  }
36129
36885
  }
@@ -36154,7 +36910,7 @@ function reportAlreadyInTranscript(transcript, report) {
36154
36910
  function parseAnnouncement(file, pid) {
36155
36911
  let fields;
36156
36912
  try {
36157
- fields = asParams(JSON.parse(readFileSync19(file, "utf8")));
36913
+ fields = asParams(JSON.parse(readFileSync21(file, "utf8")));
36158
36914
  } catch {
36159
36915
  return { kind: "rubbish" };
36160
36916
  }
@@ -36171,7 +36927,7 @@ function parseAnnouncement(file, pid) {
36171
36927
  };
36172
36928
  let announcedAt;
36173
36929
  try {
36174
- announcedAt = statSync12(file).mtimeMs;
36930
+ announcedAt = statSync13(file).mtimeMs;
36175
36931
  } catch {}
36176
36932
  const when = announcedAt === undefined ? {} : { announcedAt };
36177
36933
  if (protocol !== RUNTIME_PROTOCOL) {
@@ -36224,7 +36980,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
36224
36980
  function wentAway(file, pid) {
36225
36981
  let project = "";
36226
36982
  try {
36227
- const fields = asParams(JSON.parse(readFileSync19(file, "utf8")));
36983
+ const fields = asParams(JSON.parse(readFileSync21(file, "utf8")));
36228
36984
  project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
36229
36985
  } catch {
36230
36986
  return;
@@ -36267,29 +37023,29 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
36267
37023
  }).map((one) => one.file));
36268
37024
  }
36269
37025
  function announcedIn(directory) {
36270
- if (!existsSync20(directory)) {
37026
+ if (!existsSync21(directory)) {
36271
37027
  return [];
36272
37028
  }
36273
37029
  const found = [];
36274
37030
  const candidates = [];
36275
37031
  const entries = [];
36276
- for (const entry of readdirSync12(directory)) {
37032
+ for (const entry of readdirSync13(directory)) {
36277
37033
  const report = ERROR_REPORT_PATTERN.exec(entry);
36278
37034
  if (report) {
36279
- sweepErrorReport(join23(directory, entry), Number.parseInt(report[1] ?? "", 10));
37035
+ sweepErrorReport(join24(directory, entry), Number.parseInt(report[1] ?? "", 10));
36280
37036
  continue;
36281
37037
  }
36282
37038
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
36283
37039
  if (!match) {
36284
37040
  continue;
36285
37041
  }
36286
- const file = join23(directory, entry);
37042
+ const file = join24(directory, entry);
36287
37043
  const pid = Number.parseInt(match[1] ?? "", 10);
36288
37044
  const running = alive(pid);
36289
37045
  entries.push({ file, pid, running });
36290
37046
  if (running) {
36291
37047
  try {
36292
- candidates.push({ file, pid, writtenAt: statSync12(file).mtimeMs });
37048
+ candidates.push({ file, pid, writtenAt: statSync13(file).mtimeMs });
36293
37049
  } catch {}
36294
37050
  }
36295
37051
  }
@@ -36311,7 +37067,7 @@ function announcedIn(directory) {
36311
37067
  found.push(announced);
36312
37068
  }
36313
37069
  const held = new Set(entries.map((one) => one.file));
36314
- const here = join23(directory, ".");
37070
+ const here = join24(directory, ".");
36315
37071
  for (const file of verdicts.keys()) {
36316
37072
  if (dirname12(file) === here && !held.has(file)) {
36317
37073
  verdicts.delete(file);
@@ -36320,7 +37076,7 @@ function announcedIn(directory) {
36320
37076
  return found;
36321
37077
  }
36322
37078
  async function announcementEnded(pid, directories = runtimeDirectories()) {
36323
- const files = directories.map((directory) => join23(directory, `runtime-${pid}.json`)).filter((file) => existsSync20(file));
37079
+ const files = directories.map((directory) => join24(directory, `runtime-${pid}.json`)).filter((file) => existsSync21(file));
36324
37080
  const first = files[0];
36325
37081
  if (first === undefined) {
36326
37082
  return;
@@ -36346,7 +37102,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
36346
37102
  return;
36347
37103
  }
36348
37104
  try {
36349
- if (now - statSync12(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
37105
+ if (now - statSync13(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
36350
37106
  unlinkSync2(path);
36351
37107
  }
36352
37108
  } catch {}
@@ -36739,14 +37495,14 @@ var init_runtime_client = __esm(() => {
36739
37495
  });
36740
37496
 
36741
37497
  // src/boot-note.ts
36742
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "node:fs";
36743
- import { dirname as dirname13, join as join24 } from "node:path";
37498
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
37499
+ import { dirname as dirname13, join as join25 } from "node:path";
36744
37500
  function bootNotePath(projectPath) {
36745
- return join24(projectPath, ".godot", "gdharness-boot.json");
37501
+ return join25(projectPath, ".godot", "gdharness-boot.json");
36746
37502
  }
36747
37503
  function readScenes(projectPath) {
36748
37504
  try {
36749
- return asParams(asParams(JSON.parse(readFileSync20(bootNotePath(projectPath), "utf8")))["scenes"]);
37505
+ return asParams(asParams(JSON.parse(readFileSync22(bootNotePath(projectPath), "utf8")))["scenes"]);
36750
37506
  } catch {
36751
37507
  return {};
36752
37508
  }
@@ -36788,10 +37544,10 @@ var init_boot_note = __esm(() => {
36788
37544
  });
36789
37545
 
36790
37546
  // src/breakpoint-note.ts
36791
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync21, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
36792
- import { dirname as dirname14, isAbsolute as isAbsolute5, join as join25, relative as relative4 } from "node:path";
37547
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync23, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
37548
+ import { dirname as dirname14, isAbsolute as isAbsolute5, join as join26, relative as relative4 } from "node:path";
36793
37549
  function breakpointNotePath(projectPath) {
36794
- return join25(projectPath, ".godot", "gdharness-breakpoints.json");
37550
+ return join26(projectPath, ".godot", "gdharness-breakpoints.json");
36795
37551
  }
36796
37552
  function writeBreakpointNote(projectPath, held) {
36797
37553
  const path = breakpointNotePath(projectPath);
@@ -36817,7 +37573,7 @@ function writeBreakpointNote(projectPath, held) {
36817
37573
  }
36818
37574
  function readBreakpointNote(projectPath) {
36819
37575
  try {
36820
- const parsed = JSON.parse(readFileSync21(breakpointNotePath(projectPath), "utf8"));
37576
+ const parsed = JSON.parse(readFileSync23(breakpointNotePath(projectPath), "utf8"));
36821
37577
  if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
36822
37578
  return [];
36823
37579
  }
@@ -36832,7 +37588,7 @@ function readBreakpointNote(projectPath) {
36832
37588
  }
36833
37589
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
36834
37590
  if (lines.length > 0) {
36835
- held.push({ scriptPath: realAsFarAsItExists(join25(projectPath, script)), lines });
37591
+ held.push({ scriptPath: realAsFarAsItExists(join26(projectPath, script)), lines });
36836
37592
  }
36837
37593
  }
36838
37594
  return held;
@@ -36850,14 +37606,14 @@ var init_breakpoint_note = __esm(() => {
36850
37606
  });
36851
37607
 
36852
37608
  // src/class-note.ts
36853
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync22 } from "node:fs";
36854
- import { dirname as dirname15, join as join26 } from "node:path";
37609
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync24 } from "node:fs";
37610
+ import { dirname as dirname15, join as join27 } from "node:path";
36855
37611
  function classNotePath(projectPath) {
36856
- return join26(projectPath, ".godot", "gdharness-classes.json");
37612
+ return join27(projectPath, ".godot", "gdharness-classes.json");
36857
37613
  }
36858
37614
  function readClassNote(projectPath) {
36859
37615
  try {
36860
- const fields = asParams(JSON.parse(readFileSync22(classNotePath(projectPath), "utf8")));
37616
+ const fields = asParams(JSON.parse(readFileSync24(classNotePath(projectPath), "utf8")));
36861
37617
  const rebuiltWith = readArray(fields, "rebuiltWith");
36862
37618
  if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
36863
37619
  return null;
@@ -36899,16 +37655,16 @@ var init_class_note = __esm(() => {
36899
37655
  });
36900
37656
 
36901
37657
  // src/config-pin.ts
36902
- import { existsSync as existsSync21, readFileSync as readFileSync23 } from "node:fs";
36903
- import { join as join27 } from "node:path";
37658
+ import { existsSync as existsSync22, readFileSync as readFileSync25 } from "node:fs";
37659
+ import { join as join28 } from "node:path";
36904
37660
  function versionsNamedInConfig(projectPath) {
36905
- const config = join27(projectPath, ".mcp.json");
36906
- if (!existsSync21(config)) {
37661
+ const config = join28(projectPath, ".mcp.json");
37662
+ if (!existsSync22(config)) {
36907
37663
  return [];
36908
37664
  }
36909
37665
  let text;
36910
37666
  try {
36911
- text = readFileSync23(config, "utf8");
37667
+ text = readFileSync25(config, "utf8");
36912
37668
  JSON.parse(text);
36913
37669
  } catch {
36914
37670
  return [];
@@ -36929,10 +37685,10 @@ var init_config_pin = __esm(() => {
36929
37685
  });
36930
37686
 
36931
37687
  // src/cut-test-run.ts
36932
- import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync24, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "node:fs";
36933
- import { dirname as dirname16, join as join28 } from "node:path";
37688
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync26, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "node:fs";
37689
+ import { dirname as dirname16, join as join29 } from "node:path";
36934
37690
  function keptAt(projectPath) {
36935
- return join28(projectPath, ".godot", "gdharness-reports", "cut-run.json");
37691
+ return join29(projectPath, ".godot", "gdharness-reports", "cut-run.json");
36936
37692
  }
36937
37693
  function keepCutTestRun(projectPath, run) {
36938
37694
  const path = keptAt(projectPath);
@@ -36944,11 +37700,11 @@ function keepCutTestRun(projectPath, run) {
36944
37700
  }
36945
37701
  function takeCutTestRun(projectPath) {
36946
37702
  const path = keptAt(projectPath);
36947
- if (!existsSync22(path)) {
37703
+ if (!existsSync23(path)) {
36948
37704
  return null;
36949
37705
  }
36950
37706
  try {
36951
- const run = JSON.parse(readFileSync24(path, "utf8"));
37707
+ const run = JSON.parse(readFileSync26(path, "utf8"));
36952
37708
  rmSync12(path, { force: true });
36953
37709
  return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
36954
37710
  } catch {
@@ -37761,25 +38517,25 @@ var init_dap_client = __esm(() => {
37761
38517
  // src/editor-log.ts
37762
38518
  import {
37763
38519
  closeSync as closeSync4,
37764
- existsSync as existsSync23,
38520
+ existsSync as existsSync24,
37765
38521
  mkdirSync as mkdirSync13,
37766
38522
  openSync as openSync4,
37767
- readFileSync as readFileSync25,
38523
+ readFileSync as readFileSync27,
37768
38524
  readSync as readSync2,
37769
38525
  rmSync as rmSync13,
37770
- statSync as statSync13,
38526
+ statSync as statSync14,
37771
38527
  writeFileSync as writeFileSync14
37772
38528
  } from "node:fs";
37773
- import { dirname as dirname17, join as join29 } from "node:path";
38529
+ import { dirname as dirname17, join as join30 } from "node:path";
37774
38530
  function editorLogPath(projectPath) {
37775
- return join29(projectPath, ".godot", "gdharness-editor.log");
38531
+ return join30(projectPath, ".godot", "gdharness-editor.log");
37776
38532
  }
37777
38533
  function editorLogNotePath(projectPath) {
37778
- return join29(projectPath, ".godot", "gdharness-editor.json");
38534
+ return join30(projectPath, ".godot", "gdharness-editor.json");
37779
38535
  }
37780
38536
  function readEditorLogNote(projectPath) {
37781
38537
  try {
37782
- const fields = asParams(JSON.parse(readFileSync25(editorLogNotePath(projectPath), "utf8")));
38538
+ const fields = asParams(JSON.parse(readFileSync27(editorLogNotePath(projectPath), "utf8")));
37783
38539
  const pid = readNumber(fields, "pid");
37784
38540
  const startedAt = readString(fields, "startedAt");
37785
38541
  return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
@@ -37820,7 +38576,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
37820
38576
  return { kind: "another editor", notedPid: noted?.pid ?? null };
37821
38577
  }
37822
38578
  const path = editorLogPath(projectPath);
37823
- if (!existsSync23(path)) {
38579
+ if (!existsSync24(path)) {
37824
38580
  return { kind: "not written" };
37825
38581
  }
37826
38582
  const log = new GameLog;
@@ -37876,9 +38632,9 @@ function foldProgress(text) {
37876
38632
  `);
37877
38633
  }
37878
38634
  function theFirstOf(path) {
37879
- const whole = statSync13(path).size;
38635
+ const whole = statSync14(path).size;
37880
38636
  if (whole <= MOST_CONSOLE_BYTES) {
37881
- return { text: readFileSync25(path, "utf8") };
38637
+ return { text: readFileSync27(path, "utf8") };
37882
38638
  }
37883
38639
  const handle = openSync4(path, "r");
37884
38640
  try {
@@ -38039,7 +38795,7 @@ var init_editor_log = __esm(() => {
38039
38795
  });
38040
38796
 
38041
38797
  // src/rename.ts
38042
- import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync26, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "node:fs";
38798
+ import { existsSync as existsSync25, mkdirSync as mkdirSync14, readFileSync as readFileSync28, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "node:fs";
38043
38799
  import { dirname as dirname18, posix as posix4 } from "node:path";
38044
38800
 
38045
38801
  class Script {
@@ -38744,11 +39500,11 @@ function rewritesOf(plan, texts, move) {
38744
39500
  }
38745
39501
  function applyRewrites(rewrites) {
38746
39502
  for (const rewrite of rewrites) {
38747
- const now = readFileSync26(rewrite.absolute, "utf8");
39503
+ const now = readFileSync28(rewrite.absolute, "utf8");
38748
39504
  if (now !== rewrite.before) {
38749
39505
  throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
38750
39506
  }
38751
- if (rewrite.writeTo !== rewrite.absolute && existsSync24(rewrite.writeTo)) {
39507
+ if (rewrite.writeTo !== rewrite.absolute && existsSync25(rewrite.writeTo)) {
38752
39508
  throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
38753
39509
  }
38754
39510
  }
@@ -38762,7 +39518,7 @@ function applyRewrites(rewrites) {
38762
39518
  let movedUid = false;
38763
39519
  if (rewrite.writeTo !== rewrite.absolute) {
38764
39520
  unlinkSync3(rewrite.absolute);
38765
- if (existsSync24(`${rewrite.absolute}.uid`)) {
39521
+ if (existsSync25(`${rewrite.absolute}.uid`)) {
38766
39522
  renameSync3(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
38767
39523
  movedUid = true;
38768
39524
  }
@@ -38808,10 +39564,10 @@ var init_rename = __esm(() => {
38808
39564
  });
38809
39565
 
38810
39566
  // src/lsp_client.ts
38811
- import { existsSync as existsSync25, realpathSync as realpathSync3 } from "node:fs";
39567
+ import { existsSync as existsSync26, realpathSync as realpathSync3 } from "node:fs";
38812
39568
  import { readFile, realpath } from "node:fs/promises";
38813
39569
  import { createConnection as createConnection3 } from "node:net";
38814
- import { dirname as dirname19, join as join30, resolve as resolve6 } from "node:path";
39570
+ import { dirname as dirname19, join as join31, resolve as resolve6 } from "node:path";
38815
39571
  import { setTimeout as delay6 } from "node:timers/promises";
38816
39572
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
38817
39573
  function initializeTimeoutMs() {
@@ -39489,7 +40245,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
39489
40245
  } catch {
39490
40246
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
39491
40247
  }
39492
- if (!existsSync25(join30(projectPath, "project.godot"))) {
40248
+ if (!existsSync26(join31(projectPath, "project.godot"))) {
39493
40249
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
39494
40250
  }
39495
40251
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -39649,8 +40405,8 @@ var init_lsp_client = __esm(() => {
39649
40405
  });
39650
40406
 
39651
40407
  // src/rename-symbol.ts
39652
- import { existsSync as existsSync26, mkdirSync as mkdirSync15, readdirSync as readdirSync13, realpathSync as realpathSync4, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "node:fs";
39653
- import { basename as basename8, dirname as dirname20, join as join31, relative as relative5, resolve as resolve7 } from "node:path";
40408
+ import { existsSync as existsSync27, mkdirSync as mkdirSync15, readdirSync as readdirSync14, realpathSync as realpathSync4, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "node:fs";
40409
+ import { basename as basename8, dirname as dirname20, join as join32, relative as relative5, resolve as resolve7 } from "node:path";
39654
40410
  function resourcePathOf(projectPath, absolute) {
39655
40411
  const inside = relative5(onDiskSpelling(projectPath), onDiskSpelling(absolute));
39656
40412
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
@@ -39663,7 +40419,7 @@ function onDiskSpelling(path) {
39663
40419
  let at = resolve7(path);
39664
40420
  for (;; ) {
39665
40421
  try {
39666
- return join31(realpathSync4.native(at), ...missing.reverse());
40422
+ return join32(realpathSync4.native(at), ...missing.reverse());
39667
40423
  } catch {
39668
40424
  const parent = dirname20(at);
39669
40425
  if (parent === at) {
@@ -39717,7 +40473,7 @@ async function renameSymbol(request, services) {
39717
40473
  reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
39718
40474
  };
39719
40475
  }
39720
- if (existsSync26(request.newScriptPath)) {
40476
+ if (existsSync27(request.newScriptPath)) {
39721
40477
  return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
39722
40478
  }
39723
40479
  }
@@ -39893,7 +40649,7 @@ async function renameSymbol(request, services) {
39893
40649
  scripts,
39894
40650
  moves: new Map([[declaringPath, moveTo]]),
39895
40651
  named: [declaringPath],
39896
- exists: (path) => existsSync26(`${projectPath}/${path.slice("res://".length)}`)
40652
+ exists: (path) => existsSync27(`${projectPath}/${path.slice("res://".length)}`)
39897
40653
  }, plan);
39898
40654
  }
39899
40655
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? [] : [declaringPath]);
@@ -40092,15 +40848,15 @@ ${mention.kind}`;
40092
40848
  };
40093
40849
  }
40094
40850
  function writeLeftAloneDetail(projectPath, symbol, detail) {
40095
- const directory = join31(projectPath, ".godot", "gdharness-renames");
40851
+ const directory = join32(projectPath, ".godot", "gdharness-renames");
40096
40852
  mkdirSync15(directory, { recursive: true });
40097
40853
  detailWrites += 1;
40098
- const path = join31(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
40854
+ const path = join32(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
40099
40855
  writeFileSync16(path, `${JSON.stringify(detail, null, 2)}
40100
40856
  `, "utf8");
40101
- const kept = readdirSync13(directory).filter((name) => name.endsWith(".json")).sort().reverse();
40857
+ const kept = readdirSync14(directory).filter((name) => name.endsWith(".json")).sort().reverse();
40102
40858
  for (const old of kept.slice(10)) {
40103
- rmSync14(join31(directory, old), { force: true });
40859
+ rmSync14(join32(directory, old), { force: true });
40104
40860
  }
40105
40861
  return path;
40106
40862
  }
@@ -40141,25 +40897,25 @@ var init_rename_symbol = __esm(() => {
40141
40897
 
40142
40898
  // src/file-move.ts
40143
40899
  import {
40144
- existsSync as existsSync27,
40900
+ existsSync as existsSync28,
40145
40901
  mkdirSync as mkdirSync16,
40146
- readdirSync as readdirSync14,
40147
- readFileSync as readFileSync27,
40902
+ readdirSync as readdirSync15,
40903
+ readFileSync as readFileSync29,
40148
40904
  renameSync as renameSync4,
40149
- statSync as statSync14,
40905
+ statSync as statSync15,
40150
40906
  writeFileSync as writeFileSync17
40151
40907
  } from "node:fs";
40152
- import { basename as basename9, dirname as dirname21, extname as extname3, join as join32, relative as relative6 } from "node:path";
40908
+ import { basename as basename9, dirname as dirname21, extname as extname3, join as join33, relative as relative6 } from "node:path";
40153
40909
  function filesUnder(directory) {
40154
- return readdirSync14(directory, { withFileTypes: true }).flatMap((entry) => {
40155
- const path = join32(directory, entry.name);
40910
+ return readdirSync15(directory, { withFileTypes: true }).flatMap((entry) => {
40911
+ const path = join33(directory, entry.name);
40156
40912
  return entry.isDirectory() ? filesUnder(path) : [path];
40157
40913
  });
40158
40914
  }
40159
40915
  function sameFile(one, other) {
40160
40916
  try {
40161
- const a = statSync14(one);
40162
- const b = statSync14(other);
40917
+ const a = statSync15(one);
40918
+ const b = statSync15(other);
40163
40919
  return a.ino === b.ino && a.dev === b.dev;
40164
40920
  } catch {
40165
40921
  return false;
@@ -40173,8 +40929,8 @@ function unseenByTheEngine(projectPath, path) {
40173
40929
  return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
40174
40930
  }
40175
40931
  if (at < steps.length - 1) {
40176
- directory = join32(directory, step);
40177
- if (existsSync27(join32(directory, ".gdignore"))) {
40932
+ directory = join33(directory, step);
40933
+ if (existsSync28(join33(directory, ".gdignore"))) {
40178
40934
  return `${path} is under a directory holding a .gdignore, which the engine does not load`;
40179
40935
  }
40180
40936
  }
@@ -40186,21 +40942,21 @@ function installedAddonOf(projectPath, path) {
40186
40942
  if (steps[0] !== "addons" || steps[1] === undefined) {
40187
40943
  return null;
40188
40944
  }
40189
- return existsSync27(join32(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
40945
+ return existsSync28(join33(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
40190
40946
  }
40191
40947
  function binaryResources(projectPath) {
40192
40948
  const found = [];
40193
40949
  const visit = (directory, prefix) => {
40194
- if (existsSync27(join32(directory, ".gdignore"))) {
40950
+ if (existsSync28(join33(directory, ".gdignore"))) {
40195
40951
  return;
40196
40952
  }
40197
- for (const entry of readdirSync14(directory, { withFileTypes: true })) {
40953
+ for (const entry of readdirSync15(directory, { withFileTypes: true })) {
40198
40954
  if (entry.name.startsWith(".")) {
40199
40955
  continue;
40200
40956
  }
40201
40957
  const path = `${prefix}${entry.name}`;
40202
40958
  if (entry.isDirectory()) {
40203
- visit(join32(directory, entry.name), `${path}/`);
40959
+ visit(join33(directory, entry.name), `${path}/`);
40204
40960
  } else if (/\.(scn|res)$/i.test(entry.name)) {
40205
40961
  found.push(path);
40206
40962
  }
@@ -40210,7 +40966,7 @@ function binaryResources(projectPath) {
40210
40966
  return found;
40211
40967
  }
40212
40968
  function targetPathOf(projectPath, absolute) {
40213
- const inside = relative6(onDiskSpelling(projectPath), join32(onDiskSpelling(dirname21(absolute)), basename9(absolute)));
40969
+ const inside = relative6(onDiskSpelling(projectPath), join33(onDiskSpelling(dirname21(absolute)), basename9(absolute)));
40214
40970
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
40215
40971
  return null;
40216
40972
  }
@@ -40228,13 +40984,13 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40228
40984
  if (to === null) {
40229
40985
  return { ok: false, reason: `newPath must be a path inside the project, and ${absoluteTo} is not.` };
40230
40986
  }
40231
- if (!existsSync27(absoluteFrom)) {
40987
+ if (!existsSync28(absoluteFrom)) {
40232
40988
  return { ok: false, reason: `There is nothing at ${from} to move.` };
40233
40989
  }
40234
40990
  if (from === to) {
40235
40991
  return { ok: false, reason: `${from} is already there; nothing to move.` };
40236
40992
  }
40237
- const isDirectory = statSync14(absoluteFrom).isDirectory();
40993
+ const isDirectory = statSync15(absoluteFrom).isDirectory();
40238
40994
  if (from === "res://project.godot") {
40239
40995
  return {
40240
40996
  ok: false,
@@ -40257,7 +41013,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40257
41013
  if (isDirectory && to.startsWith(`${from}/`)) {
40258
41014
  return { ok: false, reason: `${from} cannot be moved into itself.` };
40259
41015
  }
40260
- if (existsSync27(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
41016
+ if (existsSync28(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
40261
41017
  return { ok: false, reason: `${to} already exists, so nothing was moved there.` };
40262
41018
  }
40263
41019
  const unseen = unseenByTheEngine(projectPath, to);
@@ -40265,7 +41021,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40265
41021
  return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
40266
41022
  }
40267
41023
  if (!isDirectory) {
40268
- const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync27(join32(projectPath, path.slice("res://".length))));
41024
+ const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync28(join33(projectPath, path.slice("res://".length))));
40269
41025
  if (owner !== undefined && owner !== null) {
40270
41026
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
40271
41027
  }
@@ -40287,7 +41043,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40287
41043
  } else {
40288
41044
  moves.set(from, to);
40289
41045
  for (const sidecar of SIDECARS) {
40290
- if (existsSync27(`${absoluteFrom}${sidecar}`)) {
41046
+ if (existsSync28(`${absoluteFrom}${sidecar}`)) {
40291
41047
  moves.set(`${from}${sidecar}`, `${to}${sidecar}`);
40292
41048
  }
40293
41049
  }
@@ -40299,12 +41055,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40299
41055
  scripts,
40300
41056
  moves,
40301
41057
  named: [from],
40302
- exists: (path) => existsSync27(join32(projectPath, path.slice("res://".length)))
41058
+ exists: (path) => existsSync28(join33(projectPath, path.slice("res://".length)))
40303
41059
  }, { edits: [], mentions: [] });
40304
41060
  plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
40305
41061
  const binaries = [];
40306
41062
  for (const path of binaryResources(projectPath)) {
40307
- const bytes = readFileSync27(join32(projectPath, path.slice("res://".length)));
41063
+ const bytes = readFileSync29(join33(projectPath, path.slice("res://".length)));
40308
41064
  const names = [...moves.keys()].find((moving) => bytes.includes(moving));
40309
41065
  if (names !== undefined) {
40310
41066
  binaries.push({
@@ -40323,7 +41079,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40323
41079
  }));
40324
41080
  const left = leftAloneReport(mentions);
40325
41081
  const rewrites = rewritesOf(plan, texts, null);
40326
- const absoluteOf = (path) => join32(projectPath, path.slice("res://".length));
41082
+ const absoluteOf = (path) => join33(projectPath, path.slice("res://".length));
40327
41083
  const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
40328
41084
  const moved = [...moves].map(([source, target]) => {
40329
41085
  const rewrite = rewritten.get(source);
@@ -40365,18 +41121,18 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40365
41121
  }
40366
41122
  function applyFileMove(outcome) {
40367
41123
  for (const rewrite of outcome.inPlace) {
40368
- if (readFileSync27(rewrite.absolute, "utf8") !== rewrite.before) {
41124
+ if (readFileSync29(rewrite.absolute, "utf8") !== rewrite.before) {
40369
41125
  throw new Error(`${rewrite.path} changed on disk while the move was being worked out; nothing was written`);
40370
41126
  }
40371
41127
  }
40372
41128
  for (const file of outcome.moved) {
40373
- if (!existsSync27(file.absoluteFrom)) {
41129
+ if (!existsSync28(file.absoluteFrom)) {
40374
41130
  throw new Error(`${file.from} is gone from disk; nothing was written`);
40375
41131
  }
40376
- if (file.text !== null && readFileSync27(file.absoluteFrom, "utf8") !== file.text.before) {
41132
+ if (file.text !== null && readFileSync29(file.absoluteFrom, "utf8") !== file.text.before) {
40377
41133
  throw new Error(`${file.from} changed on disk while the move was being worked out; nothing was written`);
40378
41134
  }
40379
- if (existsSync27(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
41135
+ if (existsSync28(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
40380
41136
  throw new Error(`${file.to} already exists; nothing was written`);
40381
41137
  }
40382
41138
  }
@@ -40437,7 +41193,7 @@ var init_godot_variant = __esm(() => {
40437
41193
  });
40438
41194
 
40439
41195
  // src/game-debugger.ts
40440
- var PAUSE_MS = 30000, STEP_MS = 1e4, READ_MS = 5000, REPLIES;
41196
+ var PAUSE_MS = 30000, STEP_MS = 1e4, RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", READ_MS = 5000, REPLIES;
40441
41197
  var init_game_debugger = __esm(() => {
40442
41198
  init_godot_variant();
40443
41199
  REPLIES = new Set([
@@ -44043,168 +44799,6 @@ var init_godot_bridge = __esm(() => {
44043
44799
  };
44044
44800
  });
44045
44801
 
44046
- // src/import-freshness.ts
44047
- import { createHash as createHash6 } from "node:crypto";
44048
- import { existsSync as existsSync28, readdirSync as readdirSync15, readFileSync as readFileSync28, statSync as statSync15 } from "node:fs";
44049
- import { join as join33 } from "node:path";
44050
- function md5Of(file) {
44051
- let stats;
44052
- try {
44053
- stats = statSync15(file);
44054
- } catch {
44055
- return null;
44056
- }
44057
- const known = hashes.get(file);
44058
- if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
44059
- return known.md5;
44060
- }
44061
- let md5;
44062
- try {
44063
- md5 = createHash6("md5").update(readFileSync28(file)).digest("hex");
44064
- } catch {
44065
- return null;
44066
- }
44067
- hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
44068
- return md5;
44069
- }
44070
- function sectionOf(text, section) {
44071
- const keys = new Map;
44072
- let inside = false;
44073
- for (const line of text.split(/\r?\n/)) {
44074
- const header = /^\[([^\]]+)\]\s*$/.exec(line);
44075
- if (header !== null) {
44076
- inside = header[1] === section;
44077
- continue;
44078
- }
44079
- const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
44080
- if (pair?.[1] !== undefined && pair[2] !== undefined) {
44081
- keys.set(pair[1], pair[2]);
44082
- }
44083
- }
44084
- return keys;
44085
- }
44086
- function unquoted(value) {
44087
- return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
44088
- }
44089
- function listed2(value) {
44090
- return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
44091
- }
44092
- function onDisk(projectPath, resPath) {
44093
- return join33(projectPath, resPath.slice("res://".length));
44094
- }
44095
- function behind(projectPath, resPath, sidecar) {
44096
- const remap = sectionOf(sidecar, "remap");
44097
- const importer = unquoted(remap.get("importer"));
44098
- if (importer === "keep" || importer === "skip") {
44099
- return null;
44100
- }
44101
- const outputs = listed2(sectionOf(sidecar, "deps").get("dest_files"));
44102
- if (outputs.some((output) => !existsSync28(onDisk(projectPath, output)))) {
44103
- return "its imported output is not on disk";
44104
- }
44105
- const name = resPath.slice(resPath.lastIndexOf("/") + 1);
44106
- const record = join33(projectPath, ".godot", "imported", `${name}-${createHash6("md5").update(resPath, "utf8").digest("hex")}.md5`);
44107
- let recorded = "";
44108
- try {
44109
- recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync28(record, "utf8"))?.[1] ?? "";
44110
- } catch {
44111
- recorded = "";
44112
- }
44113
- if (recorded === "") {
44114
- return "its import recorded no hash of its source";
44115
- }
44116
- const current = md5Of(onDisk(projectPath, resPath));
44117
- return current === null || current === recorded ? null : "the source changed after it was imported";
44118
- }
44119
- function staleImports(projectPath) {
44120
- const stale = [];
44121
- const visit = (directory, resDirectory) => {
44122
- if (existsSync28(join33(directory, ".gdignore")) || existsSync28(join33(directory, VERSION_MARKER))) {
44123
- return;
44124
- }
44125
- let entries;
44126
- try {
44127
- entries = readdirSync15(directory, { withFileTypes: true });
44128
- } catch {
44129
- return;
44130
- }
44131
- const names = new Set(entries.map((entry) => entry.name));
44132
- for (const entry of entries) {
44133
- if (entry.name.startsWith(".")) {
44134
- continue;
44135
- }
44136
- const resPath = `${resDirectory}${entry.name}`;
44137
- if (entry.isDirectory()) {
44138
- visit(join33(directory, entry.name), `${resPath}/`);
44139
- continue;
44140
- }
44141
- if (!entry.isFile()) {
44142
- continue;
44143
- }
44144
- if (entry.name.endsWith(".import")) {
44145
- const source = resPath.slice(0, -".import".length);
44146
- if (!names.has(entry.name.slice(0, -".import".length))) {
44147
- continue;
44148
- }
44149
- let sidecar;
44150
- try {
44151
- sidecar = readFileSync28(join33(directory, entry.name), "utf8");
44152
- } catch {
44153
- continue;
44154
- }
44155
- const reason = behind(projectPath, source, sidecar);
44156
- if (reason !== null) {
44157
- stale.push({ path: source, reason });
44158
- }
44159
- continue;
44160
- }
44161
- const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
44162
- if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
44163
- stale.push({ path: resPath, reason: "it has not been imported" });
44164
- }
44165
- }
44166
- };
44167
- visit(projectPath, "res://");
44168
- return stale.sort((one, other) => one.path.localeCompare(other.path));
44169
- }
44170
- var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
44171
- var init_import_freshness = __esm(() => {
44172
- init_project_scan();
44173
- IMPORTABLE_EXTENSIONS = [
44174
- "png",
44175
- "jpg",
44176
- "jpeg",
44177
- "webp",
44178
- "svg",
44179
- "bmp",
44180
- "tga",
44181
- "exr",
44182
- "hdr",
44183
- "dds",
44184
- "ktx",
44185
- "wav",
44186
- "mp3",
44187
- "ogg",
44188
- "ttf",
44189
- "otf",
44190
- "ttc",
44191
- "otc",
44192
- "woff",
44193
- "woff2",
44194
- "pfb",
44195
- "pfm",
44196
- "fnt",
44197
- "font",
44198
- "glb",
44199
- "gltf",
44200
- "fbx",
44201
- "obj",
44202
- "dae"
44203
- ];
44204
- IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
44205
- hashes = new Map;
44206
- });
44207
-
44208
44802
  // src/keeper-channel.ts
44209
44803
  import { connect as connect2, createServer as createServer2 } from "node:net";
44210
44804
  async function askTheKeeperToStop(address, withinMs) {
@@ -44636,7 +45230,7 @@ var init_prompts = __esm(() => {
44636
45230
  });
44637
45231
 
44638
45232
  // src/restart-note.ts
44639
- import { mkdirSync as mkdirSync18, readFileSync as readFileSync29, rmSync as rmSync15, writeFileSync as writeFileSync18 } from "node:fs";
45233
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync30, rmSync as rmSync15, writeFileSync as writeFileSync18 } from "node:fs";
44640
45234
  import { dirname as dirname23, join as join35 } from "node:path";
44641
45235
  function restartNotePath(projectPath) {
44642
45236
  return join35(projectPath, ".godot", "gdharness-restart.json");
@@ -44653,7 +45247,7 @@ function noteRestartBegun(note) {
44653
45247
  }
44654
45248
  function restartOwed(projectPath) {
44655
45249
  try {
44656
- const parsed = JSON.parse(readFileSync29(restartNotePath(projectPath), "utf8"));
45250
+ const parsed = JSON.parse(readFileSync30(restartNotePath(projectPath), "utf8"));
44657
45251
  if (typeof parsed !== "object" || parsed === null) {
44658
45252
  return null;
44659
45253
  }
@@ -44679,7 +45273,7 @@ var init_restart_note = __esm(() => {
44679
45273
 
44680
45274
  // src/run-record.ts
44681
45275
  import { createHash as createHash7 } from "node:crypto";
44682
- import { mkdirSync as mkdirSync19, openSync as openSync5, readdirSync as readdirSync16, readFileSync as readFileSync30, rmSync as rmSync16, statSync as statSync17 } from "node:fs";
45276
+ import { mkdirSync as mkdirSync19, openSync as openSync5, readdirSync as readdirSync16, readFileSync as readFileSync31, rmSync as rmSync16, statSync as statSync17 } from "node:fs";
44683
45277
  import { basename as basename10, join as join36, resolve as resolve8 } from "node:path";
44684
45278
  function runsDirectory() {
44685
45279
  return join36(runtimeDirectory(), "runs");
@@ -44703,7 +45297,7 @@ function writeLatestRun(servedBy, latest) {
44703
45297
  }
44704
45298
  function readLatestRun(servedBy, directory = runtimeDirectory()) {
44705
45299
  try {
44706
- const fields = JSON.parse(readFileSync30(latestRunPath(servedBy, directory), "utf8"));
45300
+ const fields = JSON.parse(readFileSync31(latestRunPath(servedBy, directory), "utf8"));
44707
45301
  if (typeof fields !== "object" || fields === null) {
44708
45302
  return null;
44709
45303
  }
@@ -44752,7 +45346,7 @@ function readEditorRunNote(projectPath) {
44752
45346
  function editorNoteAt(path) {
44753
45347
  let parsed;
44754
45348
  try {
44755
- parsed = JSON.parse(readFileSync30(path, "utf8"));
45349
+ parsed = JSON.parse(readFileSync31(path, "utf8"));
44756
45350
  } catch {
44757
45351
  return null;
44758
45352
  }
@@ -44802,7 +45396,7 @@ function everyRunRecord() {
44802
45396
  function recordAt(path) {
44803
45397
  let parsed;
44804
45398
  try {
44805
- parsed = JSON.parse(readFileSync30(path, "utf8"));
45399
+ parsed = JSON.parse(readFileSync31(path, "utf8"));
44806
45400
  } catch {
44807
45401
  return null;
44808
45402
  }
@@ -44861,7 +45455,7 @@ async function runningAs(pid) {
44861
45455
  const began = await startedAt(pid, null);
44862
45456
  const when = began === null ? {} : { startedAt: began };
44863
45457
  if (process.platform === "linux") {
44864
- const raw = readFileSync30(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
45458
+ const raw = readFileSync31(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
44865
45459
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
44866
45460
  }
44867
45461
  const args = (await runHidden("ps", ["-p", String(pid), "-o", "args="], { timeout: 15000 })).stdout.trim();
@@ -45049,10 +45643,10 @@ var init_run_record = __esm(() => {
45049
45643
  });
45050
45644
 
45051
45645
  // src/script-profile.ts
45052
- import { readFileSync as readFileSync31 } from "node:fs";
45646
+ import { readFileSync as readFileSync32 } from "node:fs";
45053
45647
  function readProfileFile(path) {
45054
45648
  try {
45055
- const parsed = JSON.parse(readFileSync31(path, "utf8"));
45649
+ const parsed = JSON.parse(readFileSync32(path, "utf8"));
45056
45650
  return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
45057
45651
  } catch {
45058
45652
  return null;
@@ -45109,7 +45703,7 @@ function writeProfileMark(path, mark) {
45109
45703
  }
45110
45704
  function readProfileMark(path) {
45111
45705
  try {
45112
- const parsed = JSON.parse(readFileSync31(path, "utf8"));
45706
+ const parsed = JSON.parse(readFileSync32(path, "utf8"));
45113
45707
  return typeof parsed === "object" && typeof parsed.totals === "object" ? parsed : null;
45114
45708
  } catch {
45115
45709
  return null;
@@ -45251,8 +45845,9 @@ function callersOf(totals, name, scripts) {
45251
45845
  property
45252
45846
  };
45253
45847
  }
45254
- var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", CALL_SITES = 40;
45848
+ var QUEUE_SETTING = "limits/debugger/max_queued_messages", QUEUE_DEFAULT = 2048, QUEUE_HEADROOM = 512, UNNAMED_PREFIX = "#", PROFILE_FROM_START = "GDHARNESS_PROFILE_FROM_START", ms = (seconds) => Math.round(seconds * 1e6) / 1000, RUNTIME_SCRIPT = "gdharness_runtime/runtime_autoload.gd", PROFILER_PROBE = "_profiler_probe", PROFILER_CALIBRATOR = "_calibrate_the_profiler", CALL_SITES = 40;
45255
45849
  var init_script_profile = __esm(() => {
45850
+ init_game_debugger();
45256
45851
  init_gdscript_source();
45257
45852
  init_godot_variant();
45258
45853
  init_resources();
@@ -45276,10 +45871,10 @@ var init_server_instructions = __esm(() => {
45276
45871
  });
45277
45872
 
45278
45873
  // src/test-selection.ts
45279
- import { existsSync as existsSync29, readFileSync as readFileSync32 } from "node:fs";
45874
+ import { existsSync as existsSync29, readFileSync as readFileSync33 } from "node:fs";
45280
45875
  import { basename as basename11, join as join37 } from "node:path";
45281
45876
  function casesIn(projectPath, suite) {
45282
- const text = readFileSync32(join37(projectPath, suite.slice("res://".length)), "utf8");
45877
+ const text = readFileSync33(join37(projectPath, suite.slice("res://".length)), "utf8");
45283
45878
  return [...text.matchAll(/^func\s+(test_\w*)\s*\(/gm)].map((match) => match[1] ?? "");
45284
45879
  }
45285
45880
  function suitesUnder(projectPath, asked) {
@@ -45351,7 +45946,7 @@ import {
45351
45946
  mkdirSync as mkdirSync20,
45352
45947
  openSync as openSync6,
45353
45948
  readdirSync as readdirSync17,
45354
- readFileSync as readFileSync33,
45949
+ readFileSync as readFileSync34,
45355
45950
  readSync as readSync3,
45356
45951
  rmdirSync as rmdirSync2,
45357
45952
  statSync as statSync18,
@@ -45370,6 +45965,25 @@ function waitFromEnv(variable, fallback) {
45370
45965
  function greetingWaitMs() {
45371
45966
  return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
45372
45967
  }
45968
+ function pictureToSend(pngPath, jpegPath) {
45969
+ const png = readFileSync34(pngPath);
45970
+ const beside = pngPath.replace(/\.png$/, ".jpg");
45971
+ const jpeg = jpegPath !== undefined && normalize2(jpegPath) === normalize2(beside) && existsSync30(beside) ? readFileSync34(beside) : null;
45972
+ const sent = jpeg !== null && jpeg.length < png.length ? jpeg : png;
45973
+ return {
45974
+ data: sent.toString("base64"),
45975
+ mimeType: sent === png ? "image/png" : "image/jpeg",
45976
+ bytes: sent.length,
45977
+ pngBytes: png.length
45978
+ };
45979
+ }
45980
+ function kilobytes(bytes) {
45981
+ return `${String(Math.max(1, Math.round(bytes / 1024)))} KB`;
45982
+ }
45983
+ function savedScenesOf(asked, as = "savedScenes") {
45984
+ const saved = readStringArray(asParams(JSON.parse(asked.content[0]?.text ?? "{}")), "saved");
45985
+ return saved === undefined ? {} : { [as]: saved };
45986
+ }
45373
45987
  function restartUnderway(note, now = Date.now()) {
45374
45988
  return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
45375
45989
  }
@@ -45787,7 +46401,7 @@ function endedToStartThis(ended) {
45787
46401
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
45788
46402
  }
45789
46403
  function lineBeyondScript(absolutePath, relativePath, line) {
45790
- const text = readFileSync33(absolutePath, "utf8");
46404
+ const text = readFileSync34(absolutePath, "utf8");
45791
46405
  const lines = text === "" ? 0 : text.split(`
45792
46406
  `).length - (text.endsWith(`
45793
46407
  `) ? 1 : 0);
@@ -45987,7 +46601,7 @@ function notRunNote(notRun, failFast) {
45987
46601
  function exportPresetRefusal(projectPath, preset) {
45988
46602
  let text;
45989
46603
  try {
45990
- text = readFileSync33(join38(projectPath, "export_presets.cfg"), "utf8");
46604
+ text = readFileSync34(join38(projectPath, "export_presets.cfg"), "utf8");
45991
46605
  } catch {
45992
46606
  return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
45993
46607
  }
@@ -46350,7 +46964,7 @@ function profileFrameFunctions(projectPath) {
46350
46964
  let projectGodot = null;
46351
46965
  if (projectPath !== null) {
46352
46966
  try {
46353
- projectGodot = readFileSync33(join38(projectPath, "project.godot"), "utf8");
46967
+ projectGodot = readFileSync34(join38(projectPath, "project.godot"), "utf8");
46354
46968
  } catch {}
46355
46969
  }
46356
46970
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -46374,7 +46988,7 @@ function projectSourceOf(projectPath) {
46374
46988
  let source = null;
46375
46989
  if (inside.ok) {
46376
46990
  try {
46377
- source = readFileSync33(inside.absolutePath, "utf8");
46991
+ source = readFileSync34(inside.absolutePath, "utf8");
46378
46992
  } catch {}
46379
46993
  }
46380
46994
  read.set(script, source);
@@ -46529,7 +47143,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
46529
47143
  }
46530
47144
  function gdUnitFailsOnScriptErrors(projectPath) {
46531
47145
  try {
46532
- return settingKeys(readFileSync33(join38(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
47146
+ return settingKeys(readFileSync34(join38(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
46533
47147
  } catch {
46534
47148
  return true;
46535
47149
  }
@@ -46558,12 +47172,13 @@ function elsewhereIn(projectPath, asked) {
46558
47172
  return near.length === 0 ? "" : ` The project does have ${there}.`;
46559
47173
  }
46560
47174
  function testPathsIn(projectPath, given) {
46561
- const entries = given === undefined || given === null || given === "" ? ["test"] : given;
47175
+ const defaults = TEST_DIRECTORIES.filter((directory) => existsSync30(join38(projectPath, directory)));
47176
+ const entries = given === undefined || given === null || given === "" ? defaults.length > 0 ? defaults : TEST_DIRECTORIES : given;
46562
47177
  const listed = Array.isArray(entries) ? entries : [entries];
46563
47178
  if (listed.length === 0) {
46564
47179
  return {
46565
47180
  ok: false,
46566
- reason: "path is an empty list: name a suite file or a directory, or leave path out for test."
47181
+ reason: "path is an empty list: name a suite file or a directory, or leave path out for tests and test."
46567
47182
  };
46568
47183
  }
46569
47184
  const resolved = [];
@@ -46665,11 +47280,11 @@ function scriptsUnder(directory, prefix) {
46665
47280
  return found;
46666
47281
  }
46667
47282
  function mainSceneOf(projectFile) {
46668
- const scene = parseProjectGodot(readFileSync33(projectFile, "utf8"))["application"]?.["run/main_scene"];
47283
+ const scene = parseProjectGodot(readFileSync34(projectFile, "utf8"))["application"]?.["run/main_scene"];
46669
47284
  return typeof scene === "string" && scene !== "" ? scene : null;
46670
47285
  }
46671
47286
  function editorPlaysHeadless(projectFile) {
46672
- const runArgs = parseProjectGodot(readFileSync33(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
47287
+ const runArgs = parseProjectGodot(readFileSync34(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
46673
47288
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
46674
47289
  }
46675
47290
  function ownArguments(params) {
@@ -48496,7 +49111,7 @@ class GodotServer {
48496
49111
  if (!engine.ok) {
48497
49112
  return engine.response;
48498
49113
  }
48499
- const application = parseProjectGodot(readFileSync33(project.value.file, "utf8"))["application"] ?? {};
49114
+ const application = parseProjectGodot(readFileSync34(project.value.file, "utf8"))["application"] ?? {};
48500
49115
  const name = application["config/name"];
48501
49116
  const mainScene = application["run/main_scene"];
48502
49117
  const info = {
@@ -48767,6 +49382,8 @@ class GodotServer {
48767
49382
  const userData = scratchDirectory("tests");
48768
49383
  const suitesFile = join38(userData, "suites.txt");
48769
49384
  const skippingFile = join38(userData, "skipping.txt");
49385
+ const engineLog = join38(userData, "engine.log");
49386
+ const expectedFile = join38(userData, "expected.json");
48770
49387
  writeFileSync19(suitesFile, `${(selection.value?.suites ?? asked).join(`
48771
49388
  `)}
48772
49389
  `);
@@ -48778,6 +49395,8 @@ class GodotServer {
48778
49395
  "--path",
48779
49396
  project.value.path,
48780
49397
  ...flags.value,
49398
+ "--log-file",
49399
+ engineLog,
48781
49400
  "-s",
48782
49401
  this.listedRunner,
48783
49402
  "--ignoreHeadlessMode",
@@ -48791,7 +49410,9 @@ class GodotServer {
48791
49410
  suitesFile,
48792
49411
  "--skipping-in",
48793
49412
  skippingFile,
48794
- "--html-report-off"
49413
+ "--html-report-off",
49414
+ "--expected-errors-out",
49415
+ expectedFile
48795
49416
  ];
48796
49417
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
48797
49418
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
@@ -48846,11 +49467,13 @@ class GodotServer {
48846
49467
  const reportsDir = join38(project.value.path, ".godot", ...ours.split("/"));
48847
49468
  let report = null;
48848
49469
  let reportProblem = null;
49470
+ const casePrinted = readRun(existsSync30(engineLog) ? readFileSync34(engineLog, "utf8") : "");
49471
+ const declaredErrors = readExpected(expectedFile);
48849
49472
  try {
48850
49473
  const written = existsSync30(reportsDir) && notCompiled === null ? readdirSync17(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
48851
49474
  const newest = written.at(-1);
48852
49475
  if (newest !== undefined) {
48853
- report = parseJUnit(readFileSync33(join38(reportsDir, newest, "results.xml"), "utf8"));
49476
+ report = parseJUnit(readFileSync34(join38(reportsDir, newest, "results.xml"), "utf8"));
48854
49477
  }
48855
49478
  } catch (error) {
48856
49479
  reportProblem = errorMessage(error);
@@ -48961,7 +49584,44 @@ class GodotServer {
48961
49584
  orphans: suite.orphans,
48962
49585
  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().`
48963
49586
  }));
48964
- const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.hookFailures > 0 || suite.hookErrors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
49587
+ const judgedErrors = unexpectedErrors(casePrinted, declaredErrors, [...casePrinted.suites.keys()]);
49588
+ const uncaughtTexts = new Set(uncaught.map((entry) => entry.text));
49589
+ const undeclaredPrinted = judgedErrors.printed.filter((error) => !uncaughtTexts.has(error.text));
49590
+ const undeclaredFolded = new Map;
49591
+ for (const error of undeclaredPrinted) {
49592
+ const key = `${error.suite ?? ""}
49593
+ ${error.during ?? ""}
49594
+ ${error.text}`;
49595
+ const seen = undeclaredFolded.get(key);
49596
+ if (seen === undefined) {
49597
+ undeclaredFolded.set(key, {
49598
+ suite: error.suite ?? undefined,
49599
+ case: error.during ?? undefined,
49600
+ text: error.text,
49601
+ times: 1
49602
+ });
49603
+ } else {
49604
+ seen.times += 1;
49605
+ }
49606
+ }
49607
+ const undeclaredAll = [...undeclaredFolded.values()].map((error) => ({
49608
+ ...error,
49609
+ times: error.times > 1 ? error.times : undefined
49610
+ }));
49611
+ const undeclaredErrors = undeclaredAll.slice(0, MOST_UNDECLARED_ERRORS);
49612
+ const declaredNotPrinted = judgedErrors.neverPrinted.map((missing) => ({
49613
+ suite: missing.suite,
49614
+ case: missing.during,
49615
+ text: missing.text
49616
+ }));
49617
+ const erring = (path) => ({
49618
+ undeclared: undeclaredPrinted.filter((error) => error.suite === path).length,
49619
+ notPrinted: judgedErrors.neverPrinted.filter((missing) => missing.suite === path).length
49620
+ });
49621
+ const declaredAndPrinted = casePrinted.errors.length - judgedErrors.printed.length;
49622
+ const ranIn = (suite) => suite.path === null ? suite.tests : casePrinted.suites.get(suite.path)?.cases.length ?? suite.tests;
49623
+ const notRunIn = (suite) => Math.max(0, suite.discovered - ranIn(suite));
49624
+ const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.hookFailures > 0 || suite.hookErrors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0 || erring(suite.path).undeclared + erring(suite.path).notPrinted > 0);
48965
49625
  const hooksPrinted = hookFailuresPrinted(said);
48966
49626
  const hookFailures = report.suites.filter((suite) => suite.hookFailures + suite.hookErrors > 0).flatMap((suite) => {
48967
49627
  const printedHere = hooksPrinted.filter((hook) => hook.path === suite.path);
@@ -48973,16 +49633,24 @@ class GodotServer {
48973
49633
  }
48974
49634
  ];
48975
49635
  });
48976
- const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
49636
+ const notRun = report.suites.reduce((sum, suite) => sum + notRunIn(suite), 0);
48977
49637
  const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
48978
49638
  const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
48979
49639
  const notApplied = ignoredYetRan(report, ignored);
49640
+ const scripts = ignored.length === 0 ? null : new ProjectScripts(project.value.path);
49641
+ const ignoreRead = ignoredLeftOut(report, ignored, asked.flatMap((path) => scripts === null ? [] : path.endsWith(".gd") ? scripts.suite(path) ?? [] : scripts.suitesUnder([path])));
49642
+ const unmatched = ignoreRead.unmatched.filter((entry) => !notApplied.some((one) => one.entry === entry));
48980
49643
  const testsNotRun = [
48981
49644
  ...new Set((selection.value?.selected ?? []).filter((one) => !report.suites.some((suite) => (suite.path === one.suite || suite.name === basename12(one.suite, ".gd")) && suite.cases.some((ran) => ran.name === one.test || new RegExp(`^${one.test}\\W`).test(ran.name)))).map((one) => `${basename12(one.suite, ".gd")}:${one.test}`))
48982
49645
  ];
48983
49646
  return this.jsonTextResponse({
48984
- passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0 && testsNotRun.length === 0,
48985
- verdict: testsNotRun.length === 0 ? verdict : `${verdict}; not run: ${testsNotRun.join(", ")}`,
49647
+ passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && missing.length === 0 && testsNotRun.length === 0,
49648
+ verdict: [
49649
+ verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length > 0 ? undefined : verdict,
49650
+ undeclaredPrinted.length > 0 ? `${undeclaredPrinted.length === 1 ? "one engine error" : `${String(undeclaredPrinted.length)} engine errors`} no suite declared` : undefined,
49651
+ declaredNotPrinted.length > 0 ? `${declaredNotPrinted.length === 1 ? "one declared error" : `${String(declaredNotPrinted.length)} declared errors`} never printed` : undefined,
49652
+ testsNotRun.length > 0 ? `not run: ${testsNotRun.join(", ")}` : undefined
49653
+ ].filter((part) => part !== undefined).join("; "),
48986
49654
  ...missing.length > 0 ? { missing } : {},
48987
49655
  ...testsNotRun.length > 0 ? { testsNotRun } : {},
48988
49656
  ...timedOut ? { timedOut, hung, silentForMs } : {},
@@ -48998,10 +49666,15 @@ class GodotServer {
48998
49666
  failedWithoutDetail: briefed > 0 ? briefed : undefined,
48999
49667
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
49000
49668
  uncaughtScriptErrors: uncaughtFolded.length > 0 ? uncaughtFolded : undefined,
49669
+ undeclaredErrors: undeclaredErrors.length > 0 ? undeclaredErrors : undefined,
49670
+ undeclaredErrorsOmitted: undeclaredAll.length > undeclaredErrors.length ? undeclaredAll.length - undeclaredErrors.length : undefined,
49671
+ declaredNotPrinted: declaredNotPrinted.length > 0 ? declaredNotPrinted : undefined,
49001
49672
  warnings: warnings.length > 0 ? warnings : undefined,
49002
49673
  orphans: orphans.total > 0 ? orphans.total : undefined,
49003
49674
  notRun: notRun > 0 ? notRun : undefined,
49004
49675
  ignoreNotApplied: notApplied.length > 0 ? notApplied : undefined,
49676
+ ignored: ignoreRead.leftOut.length > 0 ? ignoreRead.leftOut : undefined,
49677
+ ignoreUnmatched: unmatched.length > 0 ? unmatched : undefined,
49005
49678
  ...printedLines,
49006
49679
  savesNote,
49007
49680
  ...scanned,
@@ -49010,14 +49683,19 @@ class GodotServer {
49010
49683
  testsNotRun.length > 0 ? `${testsNotRun.join(", ")} ${testsNotRun.length === 1 ? "was" : "were"} asked for and not run, so the run is not called passed: gdUnit4 runs a test_ function only in a script that extends GdUnitTestSuite, and leaves out a suite whose script does not load.` : undefined,
49011
49684
  briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
49012
49685
  notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
49686
+ unmatched.length > 0 ? `${unmatched.join(", ")} ${unmatched.length === 1 ? "names" : "name"} no suite or case under the paths run, so ${unmatched.length === 1 ? "it" : "they"} left nothing out.` : undefined,
49013
49687
  uncaughtNote(uncaught),
49688
+ undeclaredErrors.length > 0 ? `An engine error a suite does not declare fails the run, as gdharness test judges it; ${declaringAdvice(undeclaredErrors[0]?.case ?? "test_name")}, once for each time it prints it.` : undefined,
49689
+ declaredNotPrinted.length > 0 ? "A declared error the case did not print fails the run too, since the case no longer reaches what it was written to test." : undefined,
49014
49690
  ranAs.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." : notRunNote(notRun, readBoolean(args, "failFast") === true)
49015
49691
  ].filter((part) => part !== undefined).join(" ") || undefined,
49016
49692
  suites: unclean.map((suite) => ({
49017
49693
  name: suite.name,
49018
49694
  path: suite.path,
49019
- tests: suite.tests,
49020
- notRun: suite.discovered > suite.tests ? suite.discovered - suite.tests : undefined,
49695
+ tests: ranIn(suite),
49696
+ notRun: notRunIn(suite) > 0 ? notRunIn(suite) : undefined,
49697
+ undeclaredErrors: erring(suite.path).undeclared > 0 ? erring(suite.path).undeclared : undefined,
49698
+ declaredNotPrinted: erring(suite.path).notPrinted > 0 ? erring(suite.path).notPrinted : undefined,
49021
49699
  failures: suite.failures,
49022
49700
  errors: suite.errors,
49023
49701
  hookFailures: suite.hookFailures + suite.hookErrors > 0 ? suite.hookFailures + suite.hookErrors : undefined,
@@ -49026,7 +49704,7 @@ class GodotServer {
49026
49704
  time: suite.time
49027
49705
  })),
49028
49706
  suitesPassed: report.suites.length - unclean.length,
49029
- engineErrors: run.log.count("error"),
49707
+ engineErrors: Math.max(0, run.log.count("error") - declaredAndPrinted),
49030
49708
  engineWarnings: run.log.count("warning"),
49031
49709
  engineEntries: engineEntriesShown.map((entry) => listedAsUncaught.has(entry.index) ? { ...entry, detail: undefined, detailUnder: "uncaughtScriptErrors" } : entry),
49032
49710
  engineEntriesOmitted: engineEntries.length > engineEntriesShown.length ? engineEntries.length - engineEntriesShown.length : undefined,
@@ -49242,7 +49920,7 @@ class GodotServer {
49242
49920
  return null;
49243
49921
  }
49244
49922
  try {
49245
- return readFileSync33(contained.absolutePath, "utf8");
49923
+ return readFileSync34(contained.absolutePath, "utf8");
49246
49924
  } catch {
49247
49925
  return null;
49248
49926
  }
@@ -49336,7 +50014,7 @@ class GodotServer {
49336
50014
  return null;
49337
50015
  }
49338
50016
  try {
49339
- return readFileSync33(contained.absolutePath, "utf8");
50017
+ return readFileSync34(contained.absolutePath, "utf8");
49340
50018
  } catch {
49341
50019
  return null;
49342
50020
  }
@@ -49772,8 +50450,9 @@ class GodotServer {
49772
50450
  if (servers.dapListening !== false && servers.lspListening !== false) {
49773
50451
  return { servers, report: {} };
49774
50452
  }
49775
- const { rebound, said } = await this.editorServersSaid(servers.dapListening === false);
50453
+ const { rebound, before, since } = await this.editorServersSaid(servers.dapListening === false);
49776
50454
  const after = rebound ? await this.editorServersListening() : servers;
50455
+ const said = [...before, ...since];
49777
50456
  return {
49778
50457
  servers: after,
49779
50458
  report: {
@@ -49791,17 +50470,18 @@ class GodotServer {
49791
50470
  }
49792
50471
  return {
49793
50472
  rebound: readBoolean(answer, "rebound") === true,
49794
- said: [...readStringArray(answer, "said") ?? [], ...readStringArray(answer, "saidSince") ?? []]
50473
+ before: readStringArray(answer, "said") ?? [],
50474
+ since: readStringArray(answer, "saidSince") ?? []
49795
50475
  };
49796
50476
  }
49797
50477
  async handleRebindEditorServers() {
49798
50478
  if (!this.godotBridge.getStatus().connected) {
49799
- return this.createErrorResponse("No editor is connected, so there is no debug adapter to bind.", [
49800
- "editor_launch opens one on a project",
49801
- "editor_status says whether the bridge is up and what has reached it"
49802
- ]);
50479
+ const refused = await this.noEditorYet({}, []);
50480
+ if (refused !== null) {
50481
+ return refused;
50482
+ }
49803
50483
  }
49804
- const { rebound, said } = await this.editorServersSaid(true);
50484
+ const { rebound, since: said } = await this.editorServersSaid(true);
49805
50485
  if (!rebound) {
49806
50486
  return this.createErrorResponse("The editor has no debug adapter to bind again: no DebugAdapterServer is in its tree.", ["editor_status says what the editor serves"]);
49807
50487
  }
@@ -49817,7 +50497,7 @@ class GodotServer {
49817
50497
  return null;
49818
50498
  }
49819
50499
  try {
49820
- return settingKeys(readFileSync33(join38(projectPath, "project.godot"), "utf8"));
50500
+ return settingKeys(readFileSync34(join38(projectPath, "project.godot"), "utf8"));
49821
50501
  } catch {
49822
50502
  return null;
49823
50503
  }
@@ -49865,6 +50545,7 @@ class GodotServer {
49865
50545
  serverVersion: SERVER_VERSION,
49866
50546
  addonIsStale: this.godotBridge.anotherRelease() !== undefined,
49867
50547
  staleNote: this.godotBridge.anotherRelease(),
50548
+ ...savedScenesOf(asked),
49868
50549
  ...this.whatTheEditorDropped(settingsBefore, after),
49869
50550
  ...servers,
49870
50551
  ...rebinding,
@@ -49908,6 +50589,7 @@ class GodotServer {
49908
50589
  closed: true,
49909
50590
  editorPid: pid,
49910
50591
  projectPath: before.projectPath,
50592
+ ...savedScenesOf(asked),
49911
50593
  ...this.whatTheEditorDropped(settingsBefore, this.settingKeysOf(before.projectPath)),
49912
50594
  tookMs: Date.now() - began
49913
50595
  });
@@ -49991,7 +50673,7 @@ class GodotServer {
49991
50673
  if (opened.error !== null) {
49992
50674
  return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
49993
50675
  }
49994
- return this.jsonTextResponse({ restarting: true, pid: opened.pid });
50676
+ return this.jsonTextResponse({ restarting: true, pid: opened.pid, ...savedScenesOf(asked, "saved") });
49995
50677
  }
49996
50678
  async waitForBridge(reached, deadline) {
49997
50679
  for (;; ) {
@@ -50050,7 +50732,7 @@ class GodotServer {
50050
50732
  dapPort: ports.dap,
50051
50733
  arguments: opened.arguments,
50052
50734
  finishesRestart: unfinished === null ? undefined : `This is the launch half of a restart begun at ${unfinished.quitAt} by server pid ${unfinished.byPid}, which quit editor pid ${unfinished.editorPid ?? "unknown"} and was ended before it could start one.`,
50053
- settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
50735
+ settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected, and leaves settingsDropped out when nothing was."
50054
50736
  });
50055
50737
  }
50056
50738
  async editorsNotYetConnected(projectPath) {
@@ -50542,7 +51224,7 @@ class GodotServer {
50542
51224
  return 0;
50543
51225
  }
50544
51226
  try {
50545
- return reportAlreadyInTranscript(readFileSync33(game.transcript), readFileSync33(report));
51227
+ return reportAlreadyInTranscript(readFileSync34(game.transcript), readFileSync34(report));
50546
51228
  } catch (error) {
50547
51229
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
50548
51230
  return 0;
@@ -52627,7 +53309,7 @@ class GodotServer {
52627
53309
  }
52628
53310
  const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52629
53311
  const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
52630
- if (declared.test(readFileSync33(scriptPath, "utf8"))) {
53312
+ if (declared.test(readFileSync34(scriptPath, "utf8"))) {
52631
53313
  return null;
52632
53314
  }
52633
53315
  const checked = await this.classesTheEditorCannotSee(args);
@@ -52692,6 +53374,10 @@ class GodotServer {
52692
53374
  "editor_launch open starts another"
52693
53375
  ]);
52694
53376
  }
53377
+ const closed = this.closedHere;
53378
+ if (closed !== null && (project === null || (await this.editorsNotYetConnected(project) ?? []).length === 0)) {
53379
+ return this.createErrorResponse(`Editor not connected: this server closed the editor, pid ${closed.editorPid}, on request at ${closed.at}, so nothing is coming until one is opened.`, [...instead, "editor_launch open starts one on the project"]);
53380
+ }
52695
53381
  return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
52696
53382
  ...instead,
52697
53383
  "Open the project in the editor with editor_launch, or by hand",
@@ -52840,7 +53526,8 @@ class GodotServer {
52840
53526
  }
52841
53527
  async handleRuntimeCommand(command, args, asked) {
52842
53528
  const timeoutMs = asked.patience ?? this.runtimeTimeoutMs();
52843
- const { op: _op, projectPath, pid, outputPath, ...params } = asParams(args);
53529
+ const callArgs = asParams(args);
53530
+ const { op: _op, projectPath, pid, outputPath, inline: _inline, ...params } = callArgs;
52844
53531
  const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
52845
53532
  if (waited !== null) {
52846
53533
  return this.createErrorResponse(waited, [
@@ -52873,6 +53560,13 @@ class GodotServer {
52873
53560
  }
52874
53561
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
52875
53562
  const saveTo = expectsScreenshot && typeof outputPath === "string" ? outputPath : null;
53563
+ const sendsThePicture = readBoolean(callArgs, "inline") !== false;
53564
+ if (expectsScreenshot && !sendsThePicture && saveTo === null) {
53565
+ return this.createErrorResponse("inline: false sends no picture back, so it needs outputPath to save one to; without it the capture would answer nothing to look at.", [
53566
+ "Pass outputPath, an absolute path ending in .png outside the project",
53567
+ "Or leave inline out to have the picture sent"
53568
+ ]);
53569
+ }
52876
53570
  if (saveTo !== null) {
52877
53571
  const refusal = captureDestinationRefusal(saveTo, [
52878
53572
  choice.endpoint.project.path,
@@ -52940,15 +53634,17 @@ class GodotServer {
52940
53634
  }
52941
53635
  }
52942
53636
  const ranWith = [...onGame.alongside];
53637
+ const sent = sendsThePicture ? pictureToSend(screenshotPath, readString(payload, "jpegPath")) : null;
53638
+ const how = sent === null ? ". Not sent, as inline: false asked" : sent.mimeType === "image/jpeg" ? `. Sent as JPEG, ${kilobytes(sent.bytes)} against the PNG's ${kilobytes(sent.pngBytes)}${saveTo === null ? "" : "; the saved PNG is lossless"}` : "";
52943
53639
  return {
52944
53640
  content: [
52945
53641
  {
52946
53642
  type: "text",
52947
- text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${ranWith.length === 0 ? "" : `
53643
+ text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${how}${ranWith.length === 0 ? "" : `
52948
53644
 
52949
53645
  Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}`
52950
53646
  },
52951
- { type: "image", data: readFileSync33(screenshotPath).toString("base64"), mimeType: "image/png" }
53647
+ ...sent === null ? [] : [{ type: "image", data: sent.data, mimeType: sent.mimeType }]
52952
53648
  ]
52953
53649
  };
52954
53650
  } finally {
@@ -53065,7 +53761,7 @@ async function runGodotServer() {
53065
53761
  const server = new GodotServer;
53066
53762
  await server.run();
53067
53763
  }
53068
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
53764
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
53069
53765
  var init_server = __esm(() => {
53070
53766
  init_dist();
53071
53767
  init_stdio();
@@ -53123,6 +53819,8 @@ var init_server = __esm(() => {
53123
53819
  init_server_version();
53124
53820
  init_setup();
53125
53821
  init_spawn();
53822
+ init_suite_judge();
53823
+ init_suite_plan();
53126
53824
  init_test_selection();
53127
53825
  init_tool_definitions();
53128
53826
  init_tool_reference();
@@ -53172,6 +53870,7 @@ var init_server = __esm(() => {
53172
53870
  });
53173
53871
  PROJECT_INFO_SECTIONS2 = dictionary({
53174
53872
  autoloads: { operation: "list_autoloads", params: () => ({}) },
53873
+ classes: { operation: "list_script_classes", params: () => ({}) },
53175
53874
  input_actions: { operation: "list_input_actions", params: () => ({}) },
53176
53875
  plugins: { operation: "list_plugins", params: () => ({}) },
53177
53876
  export_presets: { operation: "list_export_presets", params: () => ({}) },
@@ -54184,10 +54883,10 @@ init_setup();
54184
54883
  init_setup();
54185
54884
  init_tool_definitions();
54186
54885
  init_tool_reference();
54187
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync as readdirSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
54188
- import { basename as basename7, dirname as dirname9, join as join16 } from "node:path";
54886
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, readdirSync as readdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
54887
+ import { basename as basename7, dirname as dirname9, join as join17 } from "node:path";
54189
54888
  var SKILL_NAME = "gdharness";
54190
- var SHARED_SKILLS = join16(".agents", "skills");
54889
+ var SHARED_SKILLS = join17(".agents", "skills");
54191
54890
  function skillMarkdown(version) {
54192
54891
  return `---
54193
54892
  name: ${SKILL_NAME}
@@ -54298,6 +54997,11 @@ engines would otherwise announce sixteen games that are not games. Set \`${SCRIP
54298
54997
  true in \`project.godot\` to serve one anyway, which is for driving a \`-s\` script on purpose. Read
54299
54998
  that setting rather than assuming the quiet: it is a default and not a property of script runs.
54300
54999
 
55000
+ A scene that is not the game, such as a bench, is run as a scene and is served like the game unless
55001
+ the project lists its folder in \`${QUIET_SCENES_SETTING}\`, a \`PackedStringArray\` of \`res://\`
55002
+ prefixes, empty by default. A run started on a scene under one of them neither serves nor
55003
+ announces, nor do the engines it starts, unless it was started to be profiled from its start.
55004
+
54301
55005
  The autoload reaches an export. \`gdharness runtime off\` in the project before shipping.
54302
55006
 
54303
55007
  ## At a breakpoint
@@ -54419,27 +55123,27 @@ what they report while starting comes back under \`engine_messages_before_operat
54419
55123
  function skillFiles(version) {
54420
55124
  return new Map([
54421
55125
  ["SKILL.md", skillMarkdown(version)],
54422
- [join16("references", "tools.md"), renderToolsMarkdown()]
55126
+ [join17("references", "tools.md"), renderToolsMarkdown()]
54423
55127
  ]);
54424
55128
  }
54425
55129
  function skillDirectories(harnesses, projectPath, exists) {
54426
- const directories = [join16(projectPath, SHARED_SKILLS, SKILL_NAME)];
55130
+ const directories = [join17(projectPath, SHARED_SKILLS, SKILL_NAME)];
54427
55131
  for (const harness of harnesses) {
54428
55132
  if (harness.skills === undefined) {
54429
55133
  continue;
54430
55134
  }
54431
- const own = join16(projectPath, harness.skills.dir);
54432
- if ((!harness.skills.shared || exists(own)) && !directories.includes(join16(own, SKILL_NAME))) {
54433
- directories.push(join16(own, SKILL_NAME));
55135
+ const own = join17(projectPath, harness.skills.dir);
55136
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join17(own, SKILL_NAME))) {
55137
+ directories.push(join17(own, SKILL_NAME));
54434
55138
  }
54435
55139
  }
54436
55140
  return directories;
54437
55141
  }
54438
55142
  function everySkillDirectory(projectPath, harnesses) {
54439
- const directories = new Set([join16(projectPath, SHARED_SKILLS, SKILL_NAME)]);
55143
+ const directories = new Set([join17(projectPath, SHARED_SKILLS, SKILL_NAME)]);
54440
55144
  for (const harness of harnesses) {
54441
55145
  if (harness.skills !== undefined) {
54442
- directories.add(join16(projectPath, harness.skills.dir, SKILL_NAME));
55146
+ directories.add(join17(projectPath, harness.skills.dir, SKILL_NAME));
54443
55147
  }
54444
55148
  }
54445
55149
  return [...directories];
@@ -54447,7 +55151,7 @@ function everySkillDirectory(projectPath, harnesses) {
54447
55151
  function removeSkill(directories, projectPath) {
54448
55152
  const removed = [];
54449
55153
  for (const directory of directories) {
54450
- if (existsSync14(directory)) {
55154
+ if (existsSync15(directory)) {
54451
55155
  rmSync6(directory, { recursive: true, force: true });
54452
55156
  pruneEmpty(dirname9(directory), projectPath);
54453
55157
  removed.push(directory);
@@ -54456,10 +55160,10 @@ function removeSkill(directories, projectPath) {
54456
55160
  return removed;
54457
55161
  }
54458
55162
  function pruneEmpty(directory, projectPath) {
54459
- const shared = join16(projectPath, ".agents");
55163
+ const shared = join17(projectPath, ".agents");
54460
55164
  let path = directory;
54461
55165
  while (path !== projectPath && (basename7(path) === "skills" || path === shared)) {
54462
- if (readdirSync9(path).length > 0) {
55166
+ if (readdirSync10(path).length > 0) {
54463
55167
  return;
54464
55168
  }
54465
55169
  rmSync6(path, { recursive: true, force: true });
@@ -54469,10 +55173,10 @@ function pruneEmpty(directory, projectPath) {
54469
55173
  function writeSkill(directories, version) {
54470
55174
  const files = skillFiles(version);
54471
55175
  return directories.map((directory) => {
54472
- const replaced = existsSync14(directory);
55176
+ const replaced = existsSync15(directory);
54473
55177
  rmSync6(directory, { recursive: true, force: true });
54474
55178
  for (const [name, contents] of files) {
54475
- const path = join16(directory, name);
55179
+ const path = join17(directory, name);
54476
55180
  mkdirSync5(dirname9(path), { recursive: true });
54477
55181
  writeFileSync7(path, contents, "utf8");
54478
55182
  }
@@ -54485,27 +55189,27 @@ init_alive();
54485
55189
  init_desktop();
54486
55190
  import {
54487
55191
  closeSync as closeSync3,
54488
- existsSync as existsSync18,
55192
+ existsSync as existsSync19,
54489
55193
  fstatSync,
54490
55194
  mkdirSync as mkdirSync8,
54491
55195
  openSync as openSync3,
54492
- readdirSync as readdirSync11,
54493
- readFileSync as readFileSync17,
55196
+ readdirSync as readdirSync12,
55197
+ readFileSync as readFileSync19,
54494
55198
  readSync,
54495
55199
  rmSync as rmSync10,
54496
- statSync as statSync11,
55200
+ statSync as statSync12,
54497
55201
  writeFileSync as writeFileSync10
54498
55202
  } from "node:fs";
54499
- import { availableParallelism, freemem } from "node:os";
54500
- import { isAbsolute as isAbsolute4, join as join21, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
55203
+ import { availableParallelism, freemem, totalmem } from "node:os";
55204
+ import { isAbsolute as isAbsolute4, join as join22, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
54501
55205
  import process12 from "node:process";
54502
55206
  import { setTimeout as delay3 } from "node:timers/promises";
54503
55207
 
54504
55208
  // src/engine-slots.ts
54505
55209
  init_alive();
54506
55210
  init_process_children();
54507
- import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync13, rmSync as rmSync8, statSync as statSync9, writeSync as writeSync2 } from "node:fs";
54508
- import { join as join18 } from "node:path";
55211
+ import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync14, rmSync as rmSync8, statSync as statSync10, writeSync as writeSync2 } from "node:fs";
55212
+ import { join as join19 } from "node:path";
54509
55213
  import process11 from "node:process";
54510
55214
  import { setTimeout as delay2 } from "node:timers/promises";
54511
55215
  var START_TOLERANCE_MS = 2000;
@@ -54556,7 +55260,7 @@ class EngineSlots {
54556
55260
  for (;; ) {
54557
55261
  signal?.throwIfAborted();
54558
55262
  for (let slot = 0;slot < this.count; slot += 1) {
54559
- const lock = join18(this.directory, `${String(slot)}.lock`);
55263
+ const lock = join19(this.directory, `${String(slot)}.lock`);
54560
55264
  if (this.made(lock) || await this.takenOver(lock)) {
54561
55265
  this.held.add(lock);
54562
55266
  return lock;
@@ -54573,7 +55277,7 @@ class EngineSlots {
54573
55277
  release(lock) {
54574
55278
  this.held.delete(lock);
54575
55279
  try {
54576
- if (holderOf(readFileSync13(lock, "utf8"))?.pid === this.holder.pid) {
55280
+ if (holderOf(readFileSync14(lock, "utf8"))?.pid === this.holder.pid) {
54577
55281
  rmSync8(lock, { force: true });
54578
55282
  }
54579
55283
  } catch {}
@@ -54599,7 +55303,7 @@ class EngineSlots {
54599
55303
  const steal = `${lock}.steal`;
54600
55304
  if (!this.made(steal)) {
54601
55305
  try {
54602
- if (Date.now() - statSync9(steal).mtimeMs > UNWRITTEN_MS) {
55306
+ if (Date.now() - statSync10(steal).mtimeMs > UNWRITTEN_MS) {
54603
55307
  rmSync8(steal, { force: true });
54604
55308
  }
54605
55309
  } catch {}
@@ -54623,8 +55327,8 @@ class EngineSlots {
54623
55327
  let content;
54624
55328
  let writtenAt;
54625
55329
  try {
54626
- content = readFileSync13(lock, "utf8");
54627
- writtenAt = statSync9(lock).mtimeMs;
55330
+ content = readFileSync14(lock, "utf8");
55331
+ writtenAt = statSync10(lock).mtimeMs;
54628
55332
  } catch {
54629
55333
  return false;
54630
55334
  }
@@ -54651,9 +55355,9 @@ init_spawn();
54651
55355
 
54652
55356
  // src/suite-fingerprint.ts
54653
55357
  init_class_cache();
54654
- import { createHash as createHash5 } from "node:crypto";
54655
- import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync10 } from "node:fs";
54656
- import { join as join19, posix as posix2 } from "node:path";
55358
+ import { createHash as createHash6 } from "node:crypto";
55359
+ import { existsSync as existsSync17, readdirSync as readdirSync11, readFileSync as readFileSync15, statSync as statSync11 } from "node:fs";
55360
+ import { join as join20, posix as posix2 } from "node:path";
54657
55361
  var RES_LITERAL = /res:\/\/[^"'\s)\]<>]*/g;
54658
55362
  var RELATIVE_LOAD = /\b(?:pre)?load\(\s*["']((?!res:\/\/|uid:\/\/)[^"']+)["']/g;
54659
55363
  var REACHES_AT_RUN_TIME = [
@@ -54673,7 +55377,7 @@ class SuiteFingerprints {
54673
55377
  constructor(scripts, always) {
54674
55378
  this.scripts = scripts;
54675
55379
  this.everywhere = ["res://project.godot", ...extensionsOf(scripts.projectPath)];
54676
- this.alwaysDigest = createHash5("sha256").update(always.join(`
55380
+ this.alwaysDigest = createHash6("sha256").update(always.join(`
54677
55381
  `)).digest("hex");
54678
55382
  }
54679
55383
  of(suite) {
@@ -54690,7 +55394,7 @@ class SuiteFingerprints {
54690
55394
  }
54691
55395
  pending.push(...reached.reaches);
54692
55396
  }
54693
- const hash = createHash5("sha256").update(this.alwaysDigest);
55397
+ const hash = createHash6("sha256").update(this.alwaysDigest);
54694
55398
  for (const path of [...seen].sort()) {
54695
55399
  hash.update(`${path}
54696
55400
  ${this.read(path).digest}
@@ -54709,20 +55413,20 @@ ${this.read(path).digest}
54709
55413
  return reached;
54710
55414
  }
54711
55415
  addon(root) {
54712
- const hash = createHash5("sha256");
55416
+ const hash = createHash6("sha256");
54713
55417
  const walk = (directory, under) => {
54714
- for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
54715
- const file = join19(directory, entry.name);
55418
+ for (const entry of readdirSync11(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
55419
+ const file = join20(directory, entry.name);
54716
55420
  if (entry.isDirectory()) {
54717
55421
  walk(file, `${under}${entry.name}/`);
54718
55422
  } else if (entry.isFile()) {
54719
55423
  hash.update(`${under}${entry.name}
54720
- `).update(readFileSync14(file));
55424
+ `).update(readFileSync15(file));
54721
55425
  }
54722
55426
  }
54723
55427
  };
54724
- const directory = join19(this.scripts.projectPath, root.slice("res://".length));
54725
- if (existsSync16(directory)) {
55428
+ const directory = join20(this.scripts.projectPath, root.slice("res://".length));
55429
+ if (existsSync17(directory)) {
54726
55430
  walk(directory, "");
54727
55431
  }
54728
55432
  return { digest: hash.digest("hex"), reaches: [], unreadable: null };
@@ -54731,26 +55435,26 @@ ${this.read(path).digest}
54731
55435
  if (PLACEHOLDER.test(path)) {
54732
55436
  return { digest: "", reaches: [], unreadable: `${path} is filled in at run time` };
54733
55437
  }
54734
- const file = join19(this.scripts.projectPath, path.slice("res://".length));
54735
- if (!existsSync16(file)) {
55438
+ const file = join20(this.scripts.projectPath, path.slice("res://".length));
55439
+ if (!existsSync17(file)) {
54736
55440
  return { digest: "missing", reaches: [], unreadable: null };
54737
55441
  }
54738
- if (statSync10(file).isDirectory()) {
54739
- const inside = readdirSync10(file, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).map((entry) => `${path.endsWith("/") ? path : `${path}/`}${entry.name}`).sort();
55442
+ if (statSync11(file).isDirectory()) {
55443
+ const inside = readdirSync11(file, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).map((entry) => `${path.endsWith("/") ? path : `${path}/`}${entry.name}`).sort();
54740
55444
  return { digest: inside.join(`
54741
55445
  `), reaches: inside, unreadable: null };
54742
55446
  }
54743
55447
  if (BINARY_RESOURCE.test(path)) {
54744
55448
  return { digest: "", reaches: [], unreadable: `${path} is a binary resource` };
54745
55449
  }
54746
- const bytes = readFileSync14(file);
54747
- const digest = createHash5("sha256").update(bytes).digest("hex");
55450
+ const bytes = readFileSync15(file);
55451
+ const digest = createHash6("sha256").update(bytes).digest("hex");
54748
55452
  const text = bytes.toString("latin1");
54749
55453
  const reaches = new Set;
54750
55454
  for (const [literal] of text.matchAll(RES_LITERAL)) {
54751
55455
  reaches.add(literal);
54752
55456
  }
54753
- if (existsSync16(`${file}.import`)) {
55457
+ if (existsSync17(`${file}.import`)) {
54754
55458
  reaches.add(`${path}.import`);
54755
55459
  }
54756
55460
  if (path.endsWith(".gd")) {
@@ -54780,532 +55484,28 @@ ${this.read(path).digest}
54780
55484
  }
54781
55485
  }
54782
55486
  function extensionsOf(projectPath) {
54783
- const list = join19(projectPath, ".godot", "extension_list.cfg");
54784
- return existsSync16(list) ? readFileSync14(list, "utf8").split(`
55487
+ const list = join20(projectPath, ".godot", "extension_list.cfg");
55488
+ return existsSync17(list) ? readFileSync15(list, "utf8").split(`
54785
55489
  `).map((line) => line.trim()).filter((line) => line.startsWith("res://")) : [];
54786
55490
  }
54787
55491
 
54788
- // src/suite-judge.ts
54789
- init_junit();
54790
- import { stripVTControlCharacters } from "node:util";
54791
- var SUITE_OPENED = /^Run Test Suite:\s*(res:\/\/.+?\.gd)\s*$/;
54792
- var TIME = String.raw`(?:\d+h )?(?:\d+min )?(?:\d+s )?\d+ms`;
54793
- var CASE_LINE = new RegExp(String.raw`^\s*(res://.+?\.gd) > (.+?) (STARTED|PASSED|FAILED|FLAKY|SKIPPED|WARNING)(?: \((?:retry \d+|\d+ retries)\))?(?: (${TIME}))?\s*$`);
54794
- var STATISTICS = new RegExp(String.raw`^Statistics:.*\|\s*([A-Z]+)(?:\s+\(.*?\))?\s+(${TIME})\s*$`);
54795
- var ERROR_LINE = /^\s*(?:SCRIPT |USER )?ERROR: (.*?)\s*$/;
54796
- var EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
54797
- var DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
54798
- var LEAK = /ObjectDB instances (?:were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit/;
54799
- var STARVED = [
54800
- /Parameter "mem(?:_new)?" is null\.?\s*\n\s*at: \w+ \((?:\.\/)?core\/(?:os\/memory\.cpp|templates\/cowdata\.h):/g,
54801
- /ERROR: Failed decoding WebP image\.\s*\n\s*at: _webp_unpack/g
54802
- ];
54803
- function secondsOf(time) {
54804
- let ms = 0;
54805
- for (const [, amount, unit] of time.matchAll(/(\d+)(h|min|ms|s)\b/g)) {
54806
- ms += Number(amount) * (unit === "h" ? 3600000 : unit === "min" ? 60000 : unit === "s" ? 1000 : 1);
54807
- }
54808
- return ms / 1000;
54809
- }
54810
- function funcOf(name) {
54811
- const colon = name.indexOf(":");
54812
- return colon < 0 ? name : name.slice(0, colon);
54813
- }
54814
- function readRun(printed) {
54815
- const lines = stripVTControlCharacters(printed).replaceAll(`\r
54816
- `, `
54817
- `).split(`
54818
- `);
54819
- const suites = new Map;
54820
- const errors = [];
54821
- const leaks = [];
54822
- let executed = null;
54823
- let open = null;
54824
- let during = null;
54825
- let afterCase = [];
54826
- let lastCase = null;
54827
- const settleAfterCase = (owner) => {
54828
- for (const held of afterCase) {
54829
- errors.push({ ...held, suite: open?.path ?? null, during: owner });
54830
- }
54831
- afterCase = [];
54832
- };
54833
- for (const [at, line] of lines.entries()) {
54834
- const opened = SUITE_OPENED.exec(line);
54835
- if (opened?.[1] !== undefined) {
54836
- settleAfterCase("after");
54837
- open = suites.get(opened[1]) ?? { path: opened[1], cases: [], closed: null };
54838
- suites.set(open.path, open);
54839
- during = "before";
54840
- lastCase = null;
54841
- continue;
54842
- }
54843
- const caseLine = CASE_LINE.exec(line);
54844
- if (caseLine !== null && open !== null) {
54845
- const name = caseLine[2] ?? "";
54846
- if (caseLine[3] === "STARTED") {
54847
- settleAfterCase(lastCase ?? "before");
54848
- during = name;
54849
- } else {
54850
- open.cases.push({
54851
- name,
54852
- func: funcOf(name),
54853
- verdict: caseLine[3] ?? "",
54854
- seconds: secondsOf(caseLine[4] ?? ""),
54855
- at
54856
- });
54857
- lastCase = name;
54858
- during = null;
54859
- }
54860
- continue;
54861
- }
54862
- const statistics = STATISTICS.exec(line);
54863
- if (statistics !== null && open !== null) {
54864
- settleAfterCase("after");
54865
- open.closed = { verdict: statistics[1] ?? "", seconds: secondsOf(statistics[2] ?? "") };
54866
- open = null;
54867
- during = null;
54868
- lastCase = null;
54869
- continue;
54870
- }
54871
- const counted = EXECUTED.exec(line);
54872
- if (counted?.[1] !== undefined) {
54873
- executed = Number(counted[1]);
54874
- continue;
54875
- }
54876
- if (LEAK.test(line)) {
54877
- leaks.push(line.trim());
54878
- continue;
54879
- }
54880
- if (suites.size === 0 && DRIVER_FALLBACK.test(line)) {
54881
- errors.length = 0;
54882
- continue;
54883
- }
54884
- const error = ERROR_LINE.exec(line);
54885
- if (error === null) {
54886
- continue;
54887
- }
54888
- const text = error[1] ?? "";
54889
- if (open === null) {
54890
- errors.push({ text, at, suite: null, during: null });
54891
- } else if (during === null) {
54892
- afterCase.push({ text, at });
54893
- } else {
54894
- errors.push({ text, at, suite: open.path, during });
54895
- }
54896
- }
54897
- const last = open;
54898
- const stoppedIn = last === null ? null : during === null ? `${last.path}, after ${lastCase ?? "its cases"}` : during === "before" ? `${last.path}, in before` : `${last.path} > ${during}`;
54899
- settleAfterCase("after");
54900
- const joined = lines.join(`
54901
- `);
54902
- const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
54903
- const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
54904
- return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn };
54905
- }
54906
- var WINDOWS_EXITS = new Map([
54907
- [3221225477, "STATUS_ACCESS_VIOLATION"],
54908
- [3221225495, "STATUS_NO_MEMORY"],
54909
- [3221225773, "STATUS_COMMITMENT_LIMIT"],
54910
- [3221225794, "STATUS_DLL_INIT_FAILED"],
54911
- [3221226505, "STATUS_STACK_BUFFER_OVERRUN"],
54912
- [3228369022, "a module that could not be found (VcppException)"],
54913
- [3228369023, "a procedure that could not be found (VcppException)"]
54914
- ]);
54915
- var MEMORY_EXITS = new Set([3221225495, 3221225773]);
54916
- function exitNamed(code) {
54917
- const unsigned = code >>> 0;
54918
- const name = WINDOWS_EXITS.get(unsigned);
54919
- return name === undefined ? String(code) : `0x${unsigned.toString(16).toUpperCase()} (${name})`;
54920
- }
54921
- var TAIL = 60;
54922
- var MOST_NAMED = 8;
54923
- function named(items) {
54924
- const shown = items.slice(0, MOST_NAMED).join(", ");
54925
- return items.length > MOST_NAMED ? `${shown} and ${String(items.length - MOST_NAMED)} more` : shown;
54926
- }
54927
- function judgeEngine(printed, handed, end) {
54928
- const faults = [];
54929
- const failing = new Set;
54930
- const shownAt = new Set;
54931
- if (end.ended !== null) {
54932
- faults.push(end.ended);
54933
- } else if (end.code !== 0) {
54934
- faults.push(end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
54935
- }
54936
- if (printed.unloadable.length > 0) {
54937
- faults.push(`gdUnit4 could not load ${named(printed.unloadable)}, so it ran nothing`);
54938
- }
54939
- if (printed.executed === null || printed.executed === 0) {
54940
- faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
54941
- }
54942
- for (const { path, cases } of handed.suites) {
54943
- const suite = printed.suites.get(path);
54944
- if (suite === undefined) {
54945
- faults.push(`${path} never started`);
54946
- failing.add(path);
54947
- continue;
54948
- }
54949
- if (suite.closed === null) {
54950
- faults.push(`${path} never finished`);
54951
- failing.add(path);
54952
- } else if (suite.closed.verdict !== "PASSED") {
54953
- if (suite.cases.every((ran) => ran.verdict !== "FAILED")) {
54954
- faults.push(`${path} reported ${suite.closed.verdict} with every case passing, so a hook failed`);
54955
- }
54956
- failing.add(path);
54957
- }
54958
- const finished = new Set(suite.cases.map((ran) => ran.func));
54959
- const missing = cases.filter((func) => !finished.has(func));
54960
- if (missing.length > 0 && suite.closed !== null) {
54961
- faults.push(`${path} closed without running ${named(missing)}`);
54962
- failing.add(path);
54963
- }
54964
- const wanted = new Set(cases);
54965
- const unasked = [...finished].filter((func) => !wanted.has(func));
54966
- if (unasked.length > 0) {
54967
- faults.push(`${path} ran ${named(unasked)}, which gdharness did not read as cases of it to run here, so its ` + "plan of the cases was wrong");
54968
- failing.add(path);
54969
- }
54970
- for (const ran of suite.cases) {
54971
- if (ran.verdict === "FAILED" || ran.verdict === "FLAKY") {
54972
- faults.push(ran.verdict === "FLAKY" ? `${path} > ${ran.name} failed before it passed on a retry` : `${path} > ${ran.name} failed`);
54973
- failing.add(path);
54974
- for (let at = ran.at;at < printed.lines.length && (at === ran.at || printed.lines[at]?.trim()); at += 1) {
54975
- shownAt.add(at);
54976
- }
54977
- }
54978
- }
54979
- }
54980
- const unexpected = unexpectedErrors(printed, handed);
54981
- for (const error of unexpected.printed) {
54982
- const where = error.suite === null ? "outside every suite" : `in ${error.suite} > ${error.during ?? ""}`;
54983
- faults.push(`an engine error ${where}: ${error.text}`);
54984
- if (error.suite !== null) {
54985
- failing.add(error.suite);
54986
- }
54987
- shownAt.add(error.at);
54988
- shownAt.add(error.at + 1);
54989
- }
54990
- for (const missing of unexpected.neverPrinted) {
54991
- faults.push(`${missing.suite} > ${missing.during} declares the error "${missing.text}", which it never printed`);
54992
- failing.add(missing.suite);
54993
- }
54994
- for (const leak of printed.leaks) {
54995
- faults.push(`the engine exited holding what it should have freed: ${leak}`);
54996
- }
54997
- const memoryExit = end.code !== null && MEMORY_EXITS.has(end.code >>> 0);
54998
- const starved = memoryExit || printed.starved.length > 0;
54999
- if (printed.starved.length > 0) {
55000
- faults.push("the engine ran out of memory");
55001
- }
55002
- printed.lines.forEach((line, at) => {
55003
- if (/^(?:Overall Summary|Executed test|Total execution time| ?Exit code)/.test(line.trim())) {
55004
- shownAt.add(at);
55005
- }
55006
- });
55007
- const passed = faults.length === 0;
55008
- const passedSuites = handed.suites.map((suite) => suite.path).filter((path) => !failing.has(path));
55009
- const cutShort = end.ended !== null || printed.executed === null || handed.suites.some(({ path }) => printed.suites.get(path)?.closed == null);
55010
- return {
55011
- passed,
55012
- faults,
55013
- passedSuites,
55014
- starved,
55015
- shown: passed ? [] : linesShown(printed.lines, shownAt, cutShort)
55016
- };
55017
- }
55018
- function linesShown(lines, picked, cutShort) {
55019
- const summary = [...picked].filter((at) => at < lines.length).sort((a, b) => a - b);
55020
- const telling = summary.some((at) => !/^(?:Overall Summary|Executed test|Total execution|\s*Exit code)/.test(lines[at]?.trim() ?? ""));
55021
- if (!telling && !cutShort) {
55022
- return [];
55023
- }
55024
- if (!telling) {
55025
- return lines.slice(Math.max(0, lines.length - TAIL)).filter((line, at, all) => at < all.length - 1 || line !== "");
55026
- }
55027
- const shown = [];
55028
- let previous = -1;
55029
- for (const at of summary) {
55030
- if (previous >= 0 && at > previous + 1) {
55031
- shown.push("...");
55032
- }
55033
- shown.push(lines[at] ?? "");
55034
- previous = at;
55035
- }
55036
- return shown;
55037
- }
55038
- function unexpectedErrors(printed, handed) {
55039
- const owed = new Map;
55040
- for (const { path } of handed.suites) {
55041
- const suite = printed.suites.get(path);
55042
- const declared = handed.expected[path];
55043
- if (suite === undefined || declared === undefined) {
55044
- continue;
55045
- }
55046
- const ranHooks = suite.closed === null ? ["before"] : ["before", "after"];
55047
- for (const during of [...ranHooks, ...suite.cases.map((ran) => ran.name)]) {
55048
- const texts = declared[funcOf(during)];
55049
- if (Array.isArray(texts) && texts.length > 0) {
55050
- owed.set(`${path}
55051
- ${during}`, {
55052
- suite: path,
55053
- during,
55054
- texts: texts.map((text) => String(text))
55055
- });
55056
- }
55057
- }
55058
- }
55059
- const unexpected = [];
55060
- for (const error of printed.errors) {
55061
- const debt = error.suite === null || error.during === null ? undefined : owed.get(`${error.suite}
55062
- ${error.during}`);
55063
- const index = debt?.texts.indexOf(error.text) ?? -1;
55064
- if (debt === undefined || index < 0) {
55065
- unexpected.push(error);
55066
- } else {
55067
- debt.texts.splice(index, 1);
55068
- }
55069
- }
55070
- const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text })));
55071
- return { printed: unexpected, neverPrinted };
55072
- }
55073
-
55074
- // src/suite-plan.ts
55075
- init_class_cache();
55076
- init_gdscript_source();
55077
- init_whole_file();
55078
- import { existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync15 } from "node:fs";
55079
- import { dirname as dirname10, posix as posix3 } from "node:path";
55080
- var SUITE_BASE = "GdUnitTestSuite";
55081
- var CASE_PREFIX = "test_";
55082
- var NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*true\b/m;
55083
- var SCENE_RUNNER = /\bscene_runner\s*\(/;
55084
-
55085
- class ProjectScripts {
55086
- sources = new Map;
55087
- classes = new Map;
55088
- read = new Map;
55089
- projectPath;
55090
- constructor(projectPath) {
55091
- this.projectPath = projectPath;
55092
- eachScript(projectPath, (script, source) => {
55093
- this.sources.set(script, source);
55094
- const declared = declaredClassOf(source);
55095
- if (declared !== null) {
55096
- this.classes.set(declared, script);
55097
- }
55098
- });
55099
- }
55100
- script(path) {
55101
- const known = this.read.get(path);
55102
- if (known !== undefined) {
55103
- return known;
55104
- }
55105
- const source = this.sources.get(path);
55106
- if (source === undefined) {
55107
- return;
55108
- }
55109
- const shape = shapeOf(source);
55110
- const read = {
55111
- source,
55112
- extendsAs: shape.body.extendsAs,
55113
- funcs: shape.body.declarations.filter((d) => d.kind === "func").map((d) => d.name),
55114
- code: codeOf(source)
55115
- };
55116
- this.read.set(path, read);
55117
- return read;
55118
- }
55119
- base(path) {
55120
- const extendsAs = this.script(path)?.extendsAs ?? null;
55121
- if (extendsAs === null) {
55122
- return null;
55123
- }
55124
- const quote = extendsAs[0];
55125
- if (quote === '"' || quote === "'") {
55126
- const target = extendsAs.slice(1, -1);
55127
- return {
55128
- path: target.startsWith("res://") ? target : `res://${posix3.join(posix3.dirname(path.slice(6)), target)}`
55129
- };
55130
- }
55131
- const name = extendsAs.split(".")[0] ?? extendsAs;
55132
- const declaring = this.classes.get(name);
55133
- return declaring === undefined ? { name } : { path: declaring };
55134
- }
55135
- suite(path) {
55136
- const cases = new Set;
55137
- let windowed = false;
55138
- const seen = new Set;
55139
- let at = path;
55140
- while (!seen.has(at)) {
55141
- seen.add(at);
55142
- const read = this.script(at);
55143
- if (read === undefined) {
55144
- return null;
55145
- }
55146
- for (const func of read.funcs.filter((name) => name.startsWith(CASE_PREFIX))) {
55147
- cases.add(func);
55148
- }
55149
- windowed ||= NEEDS_WINDOW.test(read.code) || SCENE_RUNNER.test(read.code);
55150
- const base = this.base(at);
55151
- if (base === null) {
55152
- return null;
55153
- }
55154
- if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
55155
- return cases.size > 0 ? { path, cases: [...cases].sort(), windowed } : null;
55156
- }
55157
- if ("name" in base) {
55158
- return null;
55159
- }
55160
- at = base.path;
55161
- }
55162
- return null;
55163
- }
55164
- suitesUnder(roots) {
55165
- const prefixes = roots.map((root) => root.endsWith("/") ? root : `${root}/`);
55166
- return [...this.sources.keys()].filter((path) => prefixes.some((prefix) => path.startsWith(prefix))).sort().flatMap((path) => this.suite(path) ?? []);
55167
- }
55168
- }
55169
- function readTimes(file) {
55170
- try {
55171
- const parsed = JSON.parse(readFileSync15(file, "utf8"));
55172
- return typeof parsed === "object" && parsed !== null ? parsed : {};
55173
- } catch {
55174
- return {};
55175
- }
55176
- }
55177
- function writeJson(file, value) {
55178
- mkdirSync7(dirname10(file), { recursive: true });
55179
- writeWhole(file, `${JSON.stringify(value, null, 1)}
55180
- `);
55181
- }
55182
- function timesAfter(times, suites, runs) {
55183
- const next = { ...times };
55184
- const declared = new Map(suites.map((suite) => [suite.path, suite]));
55185
- const measured = new Map;
55186
- for (const run of runs) {
55187
- for (const printed of run.suites.values()) {
55188
- if (printed.closed === null || !declared.has(printed.path)) {
55189
- continue;
55190
- }
55191
- const entry = measured.get(printed.path) ?? { cases: new Map, around: 0 };
55192
- const own = new Map;
55193
- for (const ran of printed.cases) {
55194
- own.set(ran.func, (own.get(ran.func) ?? 0) + ran.seconds);
55195
- }
55196
- const inCases = [...own.values()].reduce((sum, seconds) => sum + seconds, 0);
55197
- entry.around = Math.max(entry.around, printed.closed.seconds - inCases);
55198
- for (const [func, seconds] of own) {
55199
- entry.cases.set(func, seconds);
55200
- }
55201
- measured.set(printed.path, entry);
55202
- }
55203
- }
55204
- for (const [path, entry] of measured) {
55205
- const suite = declared.get(path);
55206
- const cases = {};
55207
- for (const func of suite?.cases ?? []) {
55208
- const seconds = entry.cases.get(func) ?? times[path]?.cases[func];
55209
- if (seconds !== undefined) {
55210
- cases[func] = round(seconds);
55211
- }
55212
- }
55213
- const inCases = Object.values(cases).reduce((sum, seconds) => sum + seconds, 0);
55214
- next[path] = { seconds: round(entry.around + inCases), cases };
55215
- }
55216
- return next;
55217
- }
55218
- function round(seconds) {
55219
- return Math.round(seconds * 1000) / 1000;
55220
- }
55221
- var UNTIMED_CASE = 0.02;
55222
- var SPLIT_PAST_SECONDS = 10;
55223
- var ENGINE_START_SECONDS = 5;
55224
- function median(values) {
55225
- if (values.length === 0) {
55226
- return;
55227
- }
55228
- const sorted = [...values].sort((a, b) => a - b);
55229
- const middle = Math.floor(sorted.length / 2);
55230
- return sorted.length % 2 === 1 ? sorted[middle] : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
55231
- }
55232
- function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
55233
- const known = Object.values(times);
55234
- const caseCost = median(known.flatMap((time) => Object.values(time.cases))) ?? UNTIMED_CASE;
55235
- const aroundCost = median(known.map((time) => time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0))) ?? 0;
55236
- return suites.flatMap((suite) => {
55237
- const time = times[suite.path];
55238
- const costOf = (func) => time?.cases[func] ?? caseCost;
55239
- const around = time === undefined ? aroundCost : Math.max(0, time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0));
55240
- const whole = around + suite.cases.reduce((sum, func) => sum + costOf(func), 0);
55241
- if (suite.windowed || whole <= splitPast || suite.cases.length < 2) {
55242
- return [{ suite: suite.path, cases: suite.cases, cost: whole, windowed: suite.windowed }];
55243
- }
55244
- return suite.cases.map((func) => ({
55245
- suite: suite.path,
55246
- cases: [func],
55247
- cost: around + costOf(func),
55248
- windowed: false
55249
- }));
55250
- });
55251
- }
55252
- function dealt(units, declared, most) {
55253
- const windowed = units.filter((unit) => unit.windowed);
55254
- const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? ""));
55255
- const total = headless.reduce((sum, unit) => sum + unit.cost, 0);
55256
- const slowest = headless[0]?.cost ?? 0;
55257
- const room = Math.max(1, most - (windowed.length > 0 && most > 1 ? 1 : 0));
55258
- const count = headless.length === 0 ? 0 : Math.max(1, Math.min(room, headless.length, Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
55259
- const bins = Array.from({ length: count }, () => []);
55260
- const loads = Array.from({ length: count }, () => 0);
55261
- for (const unit of headless) {
55262
- const lightest = loads.indexOf(Math.min(...loads));
55263
- bins[lightest]?.push(unit);
55264
- loads[lightest] = (loads[lightest] ?? 0) + unit.cost;
55265
- }
55266
- const plans = bins.map((bin) => planOf(bin, declared, false));
55267
- return windowed.length > 0 ? [planOf(windowed, declared, true), ...plans] : plans;
55268
- }
55269
- function inOneEngine(units, declared) {
55270
- return planOf(units, declared, units.some((unit) => unit.windowed));
55271
- }
55272
- function planOf(bin, declared, windowed) {
55273
- const held = new Map;
55274
- for (const unit of bin) {
55275
- held.set(unit.suite, [...held.get(unit.suite) ?? [], ...unit.cases]);
55276
- }
55277
- const skipping = [...held].flatMap(([path, cases]) => (declared.get(path) ?? []).filter((func) => !cases.includes(func)).map((func) => `${path}:${func}`));
55278
- return {
55279
- suites: [...held].map(([path, cases]) => ({ path, cases: [...cases].sort() })),
55280
- skipping,
55281
- cost: bin.reduce((sum, unit) => sum + unit.cost, 0),
55282
- windowed
55283
- };
55284
- }
55285
- function viewportOf(projectPath) {
55286
- const file = `${projectPath}/project.godot`;
55287
- if (!existsSync17(file)) {
55288
- return null;
55289
- }
55290
- const text = readFileSync15(file, "utf8");
55291
- const width = /^window\/size\/viewport_width\s*=\s*(\d+)/m.exec(text)?.[1];
55292
- const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
55293
- return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
55294
- }
55492
+ // src/suite-run.ts
55493
+ init_suite_judge();
55494
+ init_suite_plan();
55295
55495
 
55296
55496
  // src/version.ts
55297
- import { readFileSync as readFileSync16 } from "node:fs";
55298
- import { dirname as dirname11, join as join20 } from "node:path";
55497
+ import { readFileSync as readFileSync18 } from "node:fs";
55498
+ import { dirname as dirname11, join as join21 } from "node:path";
55299
55499
  import { fileURLToPath as fileURLToPath2 } from "node:url";
55300
55500
  function getLocalVersion() {
55301
55501
  const currentDirectory = dirname11(fileURLToPath2(import.meta.url));
55302
55502
  const packageCandidates = [
55303
- join20(currentDirectory, "..", "package.json"),
55304
- join20(currentDirectory, "..", "..", "package.json")
55503
+ join21(currentDirectory, "..", "package.json"),
55504
+ join21(currentDirectory, "..", "..", "package.json")
55305
55505
  ];
55306
55506
  for (const packagePath of packageCandidates) {
55307
55507
  try {
55308
- const value = JSON.parse(readFileSync16(packagePath, "utf-8"));
55508
+ const value = JSON.parse(readFileSync18(packagePath, "utf-8"));
55309
55509
  if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
55310
55510
  return value.version;
55311
55511
  }
@@ -55357,9 +55557,9 @@ function testOptionsOf(args, cwd) {
55357
55557
  }
55358
55558
  const first = positional[0];
55359
55559
  const named = first === undefined ? null : resolve4(cwd, first);
55360
- const projectNamed = named !== null && existsSync18(join21(named, "project.godot"));
55560
+ const projectNamed = named !== null && existsSync19(join22(named, "project.godot"));
55361
55561
  const projectPath = projectNamed ? named : resolve4(cwd);
55362
- if (!existsSync18(join21(projectPath, "project.godot"))) {
55562
+ if (!existsSync19(join22(projectPath, "project.godot"))) {
55363
55563
  throw new TestUsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
55364
55564
  }
55365
55565
  const last = (flag) => valued.get(flag)?.at(-1);
@@ -55402,7 +55602,8 @@ function resPathOf(entry, projectPath, cwd) {
55402
55602
  const caseAt = /^(.*\.gd):(\w+)$/.exec(entry);
55403
55603
  const file = caseAt?.[1] ?? entry;
55404
55604
  const fromProject = resolve4(projectPath, file);
55405
- const absolute = isAbsolute4(file) || !existsSync18(fromProject) ? resolve4(cwd, file) : fromProject;
55605
+ const fromHere = resolve4(cwd, file);
55606
+ const absolute = isAbsolute4(file) || !existsSync19(fromProject) && existsSync19(fromHere) ? fromHere : fromProject;
55406
55607
  const inside = relative3(projectPath, absolute);
55407
55608
  if (inside.startsWith("..") || isAbsolute4(inside)) {
55408
55609
  throw new TestUsageError(`${entry} is not inside the project at ${projectPath}.`);
@@ -55414,7 +55615,7 @@ function homeOf(projectPath) {
55414
55615
  return mainCheckoutOf(projectPath) ?? projectPath;
55415
55616
  }
55416
55617
  function selectedSuites(scripts, options) {
55417
- const roots = options.roots.length > 0 ? options.roots : ["res://tests", "res://test"].filter((root) => existsSync18(join21(options.projectPath, root.slice(6))));
55618
+ const roots = options.roots.length > 0 ? options.roots : TEST_DIRECTORIES.filter((root) => existsSync19(join22(options.projectPath, root))).map((root) => `res://${root}`);
55418
55619
  if (options.selected.length === 0) {
55419
55620
  const found = scripts.suitesUnder(roots);
55420
55621
  if (found.length === 0) {
@@ -55426,10 +55627,10 @@ function selectedSuites(scripts, options) {
55426
55627
  for (const entry of options.selected) {
55427
55628
  const caseAt = /^(res:\/\/.*\.gd):(\w+)$/.exec(entry);
55428
55629
  const path = caseAt?.[1] ?? entry;
55429
- const directory = join21(options.projectPath, path.slice(6));
55430
- const suites = existsSync18(directory) && statSync11(directory).isDirectory() ? scripts.suitesUnder([path.replace(/\/$/, "")]) : [scripts.suite(path)].filter((suite) => suite !== null);
55630
+ const directory = join22(options.projectPath, path.slice(6));
55631
+ const suites = existsSync19(directory) && statSync12(directory).isDirectory() ? scripts.suitesUnder([path.replace(/\/$/, "")]) : [scripts.suite(path)].filter((suite) => suite !== null);
55431
55632
  if (suites.length === 0) {
55432
- throw new TestUsageError(existsSync18(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path}.`);
55633
+ throw new TestUsageError(existsSync19(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path} in the project at ${options.projectPath}.`);
55433
55634
  }
55434
55635
  for (const suite of suites) {
55435
55636
  const picked = chosen.get(suite.path) ?? { suite, cases: new Set };
@@ -55448,9 +55649,32 @@ function selectedSuites(scripts, options) {
55448
55649
  cases: suite.cases.filter((c) => cases.has(c))
55449
55650
  }));
55450
55651
  }
55451
- function roomFor(most) {
55452
- const byMemory = Math.floor((freemem() - MEMORY_KEPT) / MEMORY_PER_ENGINE);
55453
- return Math.max(1, Math.min(most, availableParallelism(), byMemory));
55652
+ function roomFor(most, processors, available) {
55653
+ const byMemory = Math.floor((available - MEMORY_KEPT) / MEMORY_PER_ENGINE);
55654
+ const engines = Math.max(1, Math.min(most, processors, byMemory));
55655
+ if (engines >= most) {
55656
+ return { engines, held: undefined };
55657
+ }
55658
+ const gigabytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GB`;
55659
+ return {
55660
+ engines,
55661
+ held: byMemory < processors ? `the machine has ${gigabytes(Math.max(0, available))} of memory free, ${gigabytes(MEMORY_KEPT)} of it kept for everything else and ${gigabytes(MEMORY_PER_ENGINE)} taken for each engine` : `the machine has ${plural(processors, "processor")}`
55662
+ };
55663
+ }
55664
+ async function memoryAvailable() {
55665
+ const level = await macMemoryLevel();
55666
+ return level === null ? freemem() : totalmem() * level / 100;
55667
+ }
55668
+ async function macMemoryLevel() {
55669
+ if (process12.platform !== "darwin") {
55670
+ return null;
55671
+ }
55672
+ try {
55673
+ const level = Number((await runHidden("sysctl", ["-n", "kern.memorystatus_level"])).stdout.trim());
55674
+ return Number.isFinite(level) && level > 0 && level <= 100 ? level : null;
55675
+ } catch {
55676
+ return null;
55677
+ }
55454
55678
  }
55455
55679
  function ordered(units, order) {
55456
55680
  const sorted = [...units].sort((a, b) => a.suite.localeCompare(b.suite));
@@ -55483,17 +55707,17 @@ async function runTestCommand(options, godotPath) {
55483
55707
  const startedAt = Date.now();
55484
55708
  const scripts = new ProjectScripts(options.projectPath);
55485
55709
  const home = homeOf(options.projectPath);
55486
- const state = join21(home, ".godot", "gdharness");
55487
- const timesFile = join21(state, "suite-times.json");
55488
- const passedFile = join21(state, "suite-passed.json");
55710
+ const state = join22(home, ".godot", "gdharness");
55711
+ const timesFile = join22(state, "suite-times.json");
55712
+ const passedFile = join22(state, "suite-passed.json");
55489
55713
  let suites = selectedSuites(scripts, options);
55490
55714
  const wholeSuites = suites.map((suite) => scripts.suite(suite.path) ?? suite);
55491
55715
  const declared = new Map(wholeSuites.map((suite) => [suite.path, suite.cases]));
55492
- const testRuns = join21(options.projectPath, ".godot", "gdharness", "test-runs");
55716
+ const testRuns = join22(options.projectPath, ".godot", "gdharness", "test-runs");
55493
55717
  sweepRunDirectories(testRuns);
55494
- const runDirectory = join21(testRuns, `${String(startedAt)}-${String(process12.pid)}`);
55718
+ const runDirectory = join22(testRuns, `${String(startedAt)}-${String(process12.pid)}`);
55495
55719
  mkdirSync8(runDirectory, { recursive: true });
55496
- const slots = new EngineSlots(options.slotsDirectory ?? join21(home, ".godot", "gate", "slots"), options.slotCount ?? Math.min(MOST_ENGINES, availableParallelism()), { pid: process12.pid, started: await ownStart() });
55720
+ const slots = new EngineSlots(options.slotsDirectory ?? join22(home, ".godot", "gate", "slots"), options.slotCount ?? Math.min(MOST_ENGINES, availableParallelism()), { pid: process12.pid, started: await ownStart() });
55497
55721
  const running = new Set;
55498
55722
  const interrupted = () => {
55499
55723
  for (const end of running) {
@@ -55522,9 +55746,9 @@ async function runTestCommand(options, godotPath) {
55522
55746
  }
55523
55747
  const fingerprints = new SuiteFingerprints(scripts, [
55524
55748
  getLocalVersion(),
55525
- readFileSync17(shippedListedRunner(), "utf8"),
55749
+ readFileSync19(shippedListedRunner(), "utf8"),
55526
55750
  ...options.reachAlso.map((path) => `${path}
55527
- ${existsSync18(path) ? readFileSync17(path, "latin1") : "missing"}`)
55751
+ ${existsSync19(path) ? readFileSync19(path, "latin1") : "missing"}`)
55528
55752
  ]);
55529
55753
  const printsOf = new Map(suites.map((suite) => [suite.path, fingerprints.of(suite)]));
55530
55754
  if (options.changedOnly) {
@@ -55535,7 +55759,7 @@ ${existsSync18(path) ? readFileSync17(path, "latin1") : "missing"}`)
55535
55759
  });
55536
55760
  suites = suites.filter((suite) => !unchanged.includes(suite));
55537
55761
  if (unchanged.length > 0) {
55538
- console.log(`${plural(unchanged.length, "suite")} passed with everything ${unchanged.length === 1 ? "it reaches" : "they reach"} as it stands now, and ${unchanged.length === 1 ? "is" : "are"} not run.`);
55762
+ console.log(`${plural(unchanged.length, "suite")} passed with everything ${unchanged.length === 1 ? "it reaches" : "they reach"} as it stands now, and ${unchanged.length === 1 ? "is" : "are"} not run: ${named(unchanged.map((suite) => suite.path))}.`);
55539
55763
  }
55540
55764
  if (suites.length === 0) {
55541
55765
  console.log("Nothing has changed since every suite last passed.");
@@ -55545,9 +55769,16 @@ ${existsSync18(path) ? readFileSync17(path, "latin1") : "missing"}`)
55545
55769
  }
55546
55770
  const times = readTimes(timesFile);
55547
55771
  const units = unitsOf(suites, times);
55548
- const plans = options.order === null ? dealt(units, declared, roomFor(options.engines)) : [inOneEngine(ordered(units, options.order), declared)];
55772
+ const room = roomFor(options.engines, availableParallelism(), await memoryAvailable());
55773
+ const plans = options.order === null ? dealt(units, declared, room.engines) : [inOneEngine(ordered(units, options.order), declared)];
55549
55774
  const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
55550
- console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${plans.some((plan) => plan.windowed) ? ", one of them windowed" : ""}.`);
55775
+ const held = options.order === null && room.held !== undefined && plans.length === room.engines ? `, of the ${String(options.engines)} asked for, since ${room.held}` : "";
55776
+ const how = options.order === null ? `${windowedSaid(plans)}${held}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
55777
+ console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
55778
+ const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path).length > 1);
55779
+ if (plans.length > 1 && split.length > 0) {
55780
+ console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${String(SPLIT_PAST_SECONDS)} s last time: ${named(split)}.`);
55781
+ }
55551
55782
  let ran = await Promise.all(plans.map((plan, index) => runEngine2(launch, plan, index, units)));
55552
55783
  if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
55553
55784
  console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
@@ -55570,7 +55801,7 @@ function plural(count, noun) {
55570
55801
  }
55571
55802
  function readRecord(file) {
55572
55803
  try {
55573
- const parsed = JSON.parse(readFileSync17(file, "utf8"));
55804
+ const parsed = JSON.parse(readFileSync19(file, "utf8"));
55574
55805
  return typeof parsed === "object" && parsed !== null ? parsed : {};
55575
55806
  } catch {
55576
55807
  return {};
@@ -55597,20 +55828,20 @@ function recordPassed(file, ran, printsOf) {
55597
55828
  writeJson(file, passed);
55598
55829
  }
55599
55830
  function sweepRunDirectories(testRuns) {
55600
- if (!existsSync18(testRuns)) {
55831
+ if (!existsSync19(testRuns)) {
55601
55832
  return;
55602
55833
  }
55603
- for (const entry of readdirSync11(testRuns)) {
55834
+ for (const entry of readdirSync12(testRuns)) {
55604
55835
  const pid = Number(/^\d+-(\d+)$/.exec(entry)?.[1]);
55605
55836
  if (!Number.isInteger(pid) || !alive(pid)) {
55606
- rmSync10(join21(testRuns, entry), { recursive: true, force: true });
55837
+ rmSync10(join22(testRuns, entry), { recursive: true, force: true });
55607
55838
  }
55608
55839
  }
55609
55840
  }
55610
55841
  async function importProject(launch) {
55611
- const directory = join21(launch.runDirectory, "import");
55612
- mkdirSync8(join21(directory, "user"), { recursive: true });
55613
- const log = join21(directory, "import.log");
55842
+ const directory = join22(launch.runDirectory, "import");
55843
+ mkdirSync8(join22(directory, "user"), { recursive: true });
55844
+ const log = join22(directory, "import.log");
55614
55845
  const lock = await launch.slots.take();
55615
55846
  try {
55616
55847
  const ended = await watched(launch, ["--headless", "--audio-driver", "Dummy", "--path", launch.projectPath, "--import"], directory, log, false, launch.silenceFloorSeconds);
@@ -55623,11 +55854,11 @@ async function importProject(launch) {
55623
55854
  }
55624
55855
  }
55625
55856
  async function runEngine2(launch, plan, index, units, again = "") {
55626
- const directory = join21(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
55627
- mkdirSync8(join21(directory, "user"), { recursive: true });
55628
- const suitesFile = join21(directory, "suites.txt");
55629
- const skippingFile = join21(directory, "skipping.txt");
55630
- const expectedFile = join21(directory, "expected.json");
55857
+ const directory = join22(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
55858
+ mkdirSync8(join22(directory, "user"), { recursive: true });
55859
+ const suitesFile = join22(directory, "suites.txt");
55860
+ const skippingFile = join22(directory, "skipping.txt");
55861
+ const expectedFile = join22(directory, "expected.json");
55631
55862
  writeFileSync10(suitesFile, `${plan.suites.map((suite) => suite.path).join(`
55632
55863
  `)}
55633
55864
  `);
@@ -55647,7 +55878,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55647
55878
  ...plan.windowed ? [] : ["--ignoreHeadlessMode"],
55648
55879
  "-c",
55649
55880
  "-rd",
55650
- `res://${relative3(launch.projectPath, join21(directory, "reports")).split(sep2).join("/")}`,
55881
+ `res://${relative3(launch.projectPath, join22(directory, "reports")).split(sep2).join("/")}`,
55651
55882
  "-rc",
55652
55883
  "1",
55653
55884
  "--",
@@ -55664,7 +55895,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55664
55895
  const held = new Set(plan.suites.map((suite) => suite.path));
55665
55896
  const slowest = Math.max(0, ...units.filter((unit) => held.has(unit.suite)).map((unit) => unit.cost));
55666
55897
  const silence = Math.max(launch.silenceFloorSeconds, SILENCE_TIMES_SLOWEST * slowest);
55667
- const log = join21(directory, "engine.log");
55898
+ const log = join22(directory, "engine.log");
55668
55899
  const lock = await launch.slots.take();
55669
55900
  const began = Date.now();
55670
55901
  let end;
@@ -55673,7 +55904,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55673
55904
  } finally {
55674
55905
  launch.slots.release(lock);
55675
55906
  }
55676
- const printed = readRun(readFileSync17(log, "utf8"));
55907
+ const printed = readRun(readFileSync19(log, "utf8"));
55677
55908
  const assignment = { suites: plan.suites, expected: readExpected(expectedFile) };
55678
55909
  return {
55679
55910
  index,
@@ -55684,16 +55915,8 @@ async function runEngine2(launch, plan, index, units, again = "") {
55684
55915
  seconds: (Date.now() - began) / 1000
55685
55916
  };
55686
55917
  }
55687
- function readExpected(file) {
55688
- try {
55689
- const parsed = JSON.parse(readFileSync17(file, "utf8"));
55690
- return typeof parsed === "object" && parsed !== null ? parsed : {};
55691
- } catch {
55692
- return {};
55693
- }
55694
- }
55695
55918
  async function watched(launch, args, directory, log, windowed, silenceSeconds) {
55696
- const user = join21(directory, "user");
55919
+ const user = join22(directory, "user");
55697
55920
  const env = { ...process12.env, APPDATA: user, XDG_DATA_HOME: user };
55698
55921
  const output = openSync3(log, "w");
55699
55922
  let child;
@@ -55705,12 +55928,12 @@ async function watched(launch, args, directory, log, windowed, silenceSeconds) {
55705
55928
  const how = windowed ? "on a desktop of its own" : "through the desktop helper";
55706
55929
  return { code: null, signal: null, ended: `the engine could not be started ${how}: ${compiled}` };
55707
55930
  }
55708
- const stopFile = join21(directory, "stop");
55931
+ const stopFile = join22(directory, "stop");
55709
55932
  const how = windowed ? { desktop: HIDDEN_DESKTOP, directory: launch.projectPath } : { directory: launch.projectPath };
55710
55933
  const wrapper = throughHelper(how, launch.godotPath, args, {
55711
- spec: join21(directory, "spec.json"),
55712
- pid: join21(directory, "pid"),
55713
- error: join21(directory, "error"),
55934
+ spec: join22(directory, "spec.json"),
55935
+ pid: join22(directory, "pid"),
55936
+ error: join22(directory, "error"),
55714
55937
  stop: stopFile
55715
55938
  });
55716
55939
  child = spawnHidden(wrapper.command, wrapper.args, {
@@ -55813,12 +56036,17 @@ function report(ran, startedAt, runDirectory) {
55813
56036
  const suites = new Set(ran.flatMap((engine) => engine.plan.suites.map((suite) => suite.path))).size;
55814
56037
  const longest = Math.max(0, ...ran.map((engine) => engine.seconds));
55815
56038
  const took = (Date.now() - startedAt) / 1000;
56039
+ const totals = `${plural(cases, "case")} in ${plural(suites, "suite")} across ${plural(ran.length, "engine")}, ${took.toFixed(1)} s; the longest engine took ${longest.toFixed(1)} s.`;
56040
+ console.log("");
55816
56041
  if (failed.length === 0) {
55817
56042
  rmSync10(runDirectory, { recursive: true, force: true });
56043
+ console.log(`Passed: ${totals}`);
56044
+ return 0;
55818
56045
  }
55819
- console.log("");
55820
- console.log(`${failed.length === 0 ? "Passed" : "Failed"}: ${plural(cases, "case")} in ${plural(suites, "suite")} across ${plural(ran.length, "engine")}, ${took.toFixed(1)} s; the longest engine took ${longest.toFixed(1)} s.`);
55821
- return failed.length === 0 ? 0 : 1;
56046
+ const problems = failed.reduce((sum, engine) => sum + engine.verdict.faults.length, 0);
56047
+ console.log(`Failed: ${plural(problems, "problem")}, listed above. Ran ${totals}`);
56048
+ console.log(`Every engine's output is kept under ${runDirectory} until the next run.`);
56049
+ return 1;
55822
56050
  }
55823
56051
 
55824
56052
  // src/cli.ts