{
  "id": "godot-4-headless-tests",
  "type": "loadout",
  "title": "Godot 4 headless GDScript tests",
  "summary": "A SceneTree test runner the agent runs with godot --headless after every change, failing on script errors with a non-zero exit, plus a linter for tests that can't fail.",
  "description": "godot --headless --check-only tells an agent its script parses. It doesn't tell it that the enemy never dies, that a signal fires twice, or that a node was null halfway through a test. This loadout gives the agent real tests that run in Godot's headless mode, with no editor and no window, in a second or two.\n\nThe runner is one GDScript file that extends SceneTree. It finds tests/test_*.gd, runs every test_*() method with optional before_each and after_each, prints PASS or FAIL per test and quits with exit code 1 on any failure. It hooks Godot's Logger class (4.5 and later), so a script error inside a test, such as calling a method on null, fails that test. Without the hook Godot only prints the error and the test counts as a pass. The rules cover scene tests with real frames, queue_free timing, autoload state that leaks between tests, seeded randomness and the --import step a fresh clone needs before class_name types resolve. A standard-library Python linter flags tests with no check, get_tree() in a suite, OS.delay_msec, an un-awaited frame wait and quit() inside a test.\n",
  "highlights": [
    "One-file SceneTree runner, exit code 1 on any failure, for CI and agents",
    "Script errors inside a test fail it, via Godot's Logger hook",
    "Scene tests with awaited frames, queue_free timing and autoload resets",
    "Linter flags tests that can only pass, blocking delays and un-awaited waits",
    "Catches the fresh-clone trap where class_name types are unknown until --import"
  ],
  "version": "1.0.0",
  "license": "MIT",
  "author": {
    "name": "SetupBay",
    "url": "https://setupbay.com"
  },
  "credits": [
    {
      "title": "Godot docs: command line tutorial (--headless, --script, --import)",
      "url": "https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html"
    },
    {
      "title": "Godot docs: Logger class and OS.add_logger()",
      "url": "https://docs.godotengine.org/en/stable/classes/class_logger.html"
    },
    {
      "title": "Godot docs: SceneTree (process_frame, quit)",
      "url": "https://docs.godotengine.org/en/stable/classes/class_scenetree.html"
    },
    {
      "title": "@Spidermanzdev: 65 test suites run headless from the Godot CLI after every change",
      "url": "https://x.com/Spidermanzdev/status/2115589607161004357"
    },
    {
      "title": "@jvan253: signal timing bugs parse fine, so make the agent run the project headless",
      "url": "https://x.com/jvan253/status/2114034185627406820"
    },
    {
      "title": "@GrizzlyKiwidev: if anything throws an error headless, the agent knows its change broke it",
      "url": "https://x.com/GrizzlyKiwidev/status/2114440921836322827"
    }
  ],
  "categories": [
    "gamedev",
    "workflow"
  ],
  "stacks": [
    "godot",
    "gdscript"
  ],
  "files": [
    {
      "path": "AGENTS.md",
      "role": "instructions",
      "description": "Commands, test layout, rules for writing headless tests, and the done checklist.",
      "skill": null,
      "agent": null,
      "install_to": {
        "claude-code": "AGENTS.md",
        "codex": "AGENTS.md",
        "cursor": "AGENTS.md",
        "gemini-cli": "AGENTS.md",
        "antigravity-cli": "AGENTS.md",
        "copilot": "AGENTS.md"
      },
      "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/AGENTS.md",
      "raw_latest_url": "https://setupbay.com/raw/godot-4-headless-tests/latest/AGENTS.md",
      "bytes": 3346,
      "lang": "markdown",
      "content": "# Godot 4 headless tests\n\nThis project's game rules are checked by GDScript tests that run in Godot's headless mode, with no\neditor and no window. Run them after every change and before you say a change is done. A script that\nparses can still be wrong: signal timing, freed nodes and off-by-one damage all parse fine.\n\n## Commands\n\n```bash\ngodot --headless --import --path .            # once per fresh clone or new class_name; builds .godot/\ngodot --headless --path . --script res://tests/run_tests.gd   # runs every test, exit 1 on any failure\npython3 .agents/skills/godot-headless-tests/scripts/gd_test_lint.py tests   # catches tests that can't fail\n```\n\nUse the Godot 4 binary the project pins (`config/features` in project.godot). If it isn't on PATH,\nask for its path rather than downloading one. Wrap the run in `timeout 300` in CI so a hung test fails.\n\n## Layout\n\n- `tests/run_tests.gd`: the runner (copy it from the skill's `references/run_tests.gd`). It extends\n  SceneTree, finds `tests/test_*.gd`, runs every `test_*()` method, prints `PASS`/`FAIL` per test\n  and quits with exit code 1 if anything failed.\n- `tests/test_<thing>.gd`: one suite per script or scene. A suite `extends RefCounted` and declares\n  `var t: SceneTree`; the runner sets `t` to itself before each test.\n- Optional `before_each()` and `after_each()` run around every test; they may `await`.\n\n## Writing tests\n\n- Check with `t.equal(actual, expected, \"label\")` and `t.check(condition, \"message\")`. Every test\n  needs at least one. `print()` is not a check.\n- A script error inside a test (calling a method on null, a bad cast) marks that test as failed. The\n  runner hooks Godot's Logger for this, because otherwise Godot only prints the error and the test\n  looks like a pass. Don't \"fix\" a failing test by guarding the null; find out why it was null.\n- Add nodes under `t.root`, never `get_tree()` (a RefCounted suite isn't in the tree). Free what you\n  add in `after_each()`.\n- Scene tests: `load(\"res://scenes/x.tscn\").instantiate()`, add to `t.root`, then\n  `await t.frames(1)` so `_ready()` and the first process frame have run. `queue_free()` only happens\n  at the end of a frame, so await a frame before you check a node is gone.\n- Always `await t.frames(n)`. Never `OS.delay_msec()`, which blocks the main loop so nothing moves,\n  and avoid `create_timer()` waits: call the timeout handler, or set the Timer's `wait_time` tiny.\n- Seed randomness: pass a `RandomNumberGenerator` with a fixed `seed` into the code under test.\n- Never call `quit()` from a test. The runner owns the exit code.\n- Autoloads from project.godot are loaded in a `--script` run (Godot 4.7) and live for the whole\n  run, so state leaks between tests. Reset whatever a test changes in `before_each()`.\n- Editor-only code (EditorInterface, EditorPlugin) can't be exercised here. Keep plugin UI thin.\n\nIf the project already uses GUT or gdUnit4, use that instead and keep these rules for the tests.\n\n## Done means\n\n1. `--import` has run since the last new `class_name`, or the run fails with \"Could not find type\".\n2. The lint script reports 0 errors on `tests/`.\n3. The run ends with `0 failed` and exits 0. Add or change a test for every rule you change, and\n   say which test covers it.\n4. List anything that still needs a human play-test (feel, visuals, audio, input devices).\n"
    },
    {
      "path": "CLAUDE.md",
      "role": "adapter",
      "description": "Imports AGENTS.md for Claude Code.",
      "skill": null,
      "agent": "claude-code",
      "install_to": {
        "claude-code": "CLAUDE.md",
        "codex": null,
        "cursor": null,
        "gemini-cli": null,
        "antigravity-cli": null,
        "copilot": null
      },
      "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/CLAUDE.md",
      "raw_latest_url": "https://setupbay.com/raw/godot-4-headless-tests/latest/CLAUDE.md",
      "bytes": 380,
      "lang": "markdown",
      "content": "# CLAUDE.md\n\n@AGENTS.md\n\n## Claude Code notes\n\n- Skills from this loadout live in `.claude/skills/`: `godot-headless-tests`. Use them when their description matches the task.\n- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.\n\n<!-- From SetupBay \"Godot 4 headless GDScript tests\" v1.0.0 — https://setupbay.com/loadouts/godot-4-headless-tests/ -->\n"
    },
    {
      "path": "GEMINI.md",
      "role": "adapter",
      "description": "Imports AGENTS.md for Gemini CLI.",
      "skill": null,
      "agent": "gemini-cli",
      "install_to": {
        "claude-code": null,
        "codex": null,
        "cursor": null,
        "gemini-cli": "GEMINI.md",
        "antigravity-cli": null,
        "copilot": null
      },
      "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/GEMINI.md",
      "raw_latest_url": "https://setupbay.com/raw/godot-4-headless-tests/latest/GEMINI.md",
      "bytes": 381,
      "lang": "markdown",
      "content": "# GEMINI.md\n\n@./AGENTS.md\n\n## Gemini CLI notes\n\n- Skills from this loadout live in `.agents/skills/`: `godot-headless-tests`. Use them when their description matches the task.\n- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.\n\n<!-- From SetupBay \"Godot 4 headless GDScript tests\" v1.0.0 — https://setupbay.com/loadouts/godot-4-headless-tests/ -->\n"
    },
    {
      "path": ".cursor/rules/godot-headless-tests.mdc",
      "role": "rules",
      "description": "Never/instead table for GDScript test suites under tests/.",
      "skill": null,
      "agent": null,
      "install_to": {
        "claude-code": null,
        "codex": null,
        "cursor": ".cursor/rules/godot-headless-tests.mdc",
        "gemini-cli": null,
        "antigravity-cli": null,
        "copilot": null
      },
      "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/.cursor/rules/godot-headless-tests.mdc",
      "raw_latest_url": "https://setupbay.com/raw/godot-4-headless-tests/latest/.cursor/rules/godot-headless-tests.mdc",
      "bytes": 1191,
      "lang": "markdown",
      "content": "---\ndescription: Writing Godot 4 GDScript tests that run headless with tests/run_tests.gd\nglobs: \"tests/**/*.gd\"\nalwaysApply: false\n---\n\n# Godot 4 headless tests\n\nRun: `godot --headless --path . --script res://tests/run_tests.gd` (after `--import` on a fresh clone).\n\n| Never | Instead |\n| --- | --- |\n| A `test_*()` with no check, or `print()` as the check | `t.equal(actual, expected, \"label\")`, `t.check(cond, \"msg\")` |\n| `get_tree()` in a suite | `t.root.add_child(node)`; free it in `after_each()` |\n| `OS.delay_msec()` | `await t.frames(n)` |\n| `t.frames(n)` without `await` | `await t.frames(n)` |\n| `await t.create_timer(2.0).timeout` | Call the timeout handler, or set `wait_time` tiny |\n| `randi()` / `randf_range()` in the code under test | A seeded `RandomNumberGenerator` passed in |\n| `t.quit()` or `get_tree().quit()` in a test | Let the runner exit with the right code |\n| Assuming an autoload starts fresh for each test | Reset its state in `before_each()`; it lives for the whole run |\n| Guarding a null so a red test goes green | Find why it was null; script errors fail the test on purpose |\n\nEach suite: `extends RefCounted`, `var t: SceneTree`, methods named `test_*`.\n"
    },
    {
      "path": "skills/godot-headless-tests/SKILL.md",
      "role": "skill",
      "description": "Set up the runner, write a suite, lint it, run it headless and report.",
      "skill": "godot-headless-tests",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/godot-headless-tests/SKILL.md",
        "codex": ".agents/skills/godot-headless-tests/SKILL.md",
        "cursor": ".agents/skills/godot-headless-tests/SKILL.md",
        "gemini-cli": ".agents/skills/godot-headless-tests/SKILL.md",
        "antigravity-cli": ".agents/skills/godot-headless-tests/SKILL.md",
        "copilot": ".agents/skills/godot-headless-tests/SKILL.md"
      },
      "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/SKILL.md",
      "raw_latest_url": "https://setupbay.com/raw/godot-4-headless-tests/latest/skills/godot-headless-tests/SKILL.md",
      "bytes": 2712,
      "lang": "markdown",
      "content": "---\nname: godot-headless-tests\ndescription: Set up and run GDScript tests for a Godot 4 project in headless mode, with a SceneTree runner that fails on script errors and exits non-zero. Use when adding a test, changing game rules, or before saying a Godot change is done.\n---\n\n# Godot 4 headless tests\n\n## 1. Set up once\n\nIf `tests/run_tests.gd` doesn't exist, copy `references/run_tests.gd` from this skill to\n`tests/run_tests.gd`. It needs Godot 4.5 or later (it uses the `Logger` class); check\n`config/features` in project.godot. Then build the import cache:\n\n```bash\ngodot --headless --import --path .\n```\n\nRun `--import` again whenever you add a `class_name` or a new resource. Without it a fresh clone\nfails with `Parse Error: Could not find type \"Health\"` even though the code is fine.\n\n## 2. Write the test\n\nOne suite per script or scene, in `tests/test_<thing>.gd`:\n\n```gdscript\nextends RefCounted\n\nvar t: SceneTree   # set by the runner\nvar health: Health\n\n\nfunc before_each() -> void:\n\thealth = Health.new()\n\thealth.maximum = 50\n\tt.root.add_child(health)   # runs _ready()\n\n\nfunc after_each() -> void:\n\thealth.free()\n\n\nfunc test_damage_clamps_at_zero_and_emits_died_once() -> void:\n\tvar deaths := [0]\n\thealth.died.connect(func() -> void: deaths[0] += 1)\n\thealth.damage(80)\n\thealth.damage(10)\n\tt.equal(health.current, 0)\n\tt.equal(deaths[0], 1, \"died emitted\")\n```\n\nFor a scene, instance it, add it under `t.root`, then `await t.frames(1)` before you poke it.\nAfter `queue_free()`, `await t.frames(1)` before checking the node has gone. Capture signal counts\nin an array (lambdas capture by value, so a plain `int` won't update).\n\n## 3. Lint the tests\n\n```bash\npython3 scripts/gd_test_lint.py tests\n```\n\nErrors: a test with no check, `get_tree()` in a suite, `OS.delay_msec()`, `t.frames()` without\n`await`, `quit()` in a test, editor-only APIs. Warnings: `create_timer()` waits, unseeded random\ncalls, `print()` used as a check, bare `assert()`. Fix errors; fix or justify warnings\n(`# lint: ok` on the line). Try it on `references/bad-test-sample.gd.txt` (7 errors, 4 warnings).\n\n## 4. Run\n\n```bash\ntimeout 300 godot --headless --path . --script res://tests/run_tests.gd\n```\n\nEach test prints `PASS suite::test_name` or `FAIL suite::test_name: reason`, then\n`N passed, M failed`. Exit code 1 means at least one failure. A script error inside a test is a\nfailure with the message and line, such as `script error: Cannot call method 'get_name' on a null\nvalue. (test_arena.gd:14)`. A suite that doesn't parse shows as `FAIL test_x.gd: script did not\nload`.\n\n## 5. Report\n\nGive the last line of the run, the tests you added or changed and which rule each covers, and\nanything left for a human play-test.\n"
    },
    {
      "path": "skills/godot-headless-tests/references/run_tests.gd",
      "role": "reference",
      "description": "The SceneTree test runner to copy to tests/run_tests.gd.",
      "skill": "godot-headless-tests",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/godot-headless-tests/references/run_tests.gd",
        "codex": ".agents/skills/godot-headless-tests/references/run_tests.gd",
        "cursor": ".agents/skills/godot-headless-tests/references/run_tests.gd",
        "gemini-cli": ".agents/skills/godot-headless-tests/references/run_tests.gd",
        "antigravity-cli": ".agents/skills/godot-headless-tests/references/run_tests.gd",
        "copilot": ".agents/skills/godot-headless-tests/references/run_tests.gd"
      },
      "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/run_tests.gd",
      "raw_latest_url": "https://setupbay.com/raw/godot-4-headless-tests/latest/skills/godot-headless-tests/references/run_tests.gd",
      "bytes": 3121,
      "lang": "markdown",
      "content": "extends SceneTree\n## Headless test runner: godot --headless --path . --script res://tests/run_tests.gd\n## Runs every test_*() method in res://tests/test_*.gd, prints PASS/FAIL, and exits 1 on any failure.\n\nconst TEST_DIR := \"res://tests\"\n\nvar _failures: Array[String] = []\nvar _current := \"\"\nvar _errors := ErrorCatcher.new()\n\n\n## Script errors (null calls, bad casts, failed asserts) only print by default: the test\n## function stops, but the run still counts it as passed. This logger turns each one into a\n## failure of the running test. Warnings are ignored.\nclass ErrorCatcher extends Logger:\n\tvar _lock := Mutex.new()\n\tvar _seen: Array[String] = []\n\n\tfunc _log_error(_function: String, file: String, line: int, code: String, rationale: String, _editor_notify: bool, error_type: int, _script_backtrace: Array[ScriptBacktrace]) -> void:\n\t\tif error_type == ERROR_TYPE_WARNING:\n\t\t\treturn\n\t\t_lock.lock()\n\t\t_seen.append(\"%s (%s:%d)\" % [rationale if not rationale.is_empty() else code, file.get_file(), line])\n\t\t_lock.unlock()\n\n\tfunc take() -> Array[String]:\n\t\t_lock.lock()\n\t\tvar out := _seen.duplicate()\n\t\t_seen.clear()\n\t\t_lock.unlock()\n\t\treturn out\n\n\nfunc _initialize() -> void:\n\tOS.add_logger(_errors)\n\t_run.call_deferred()\n\n\nfunc _run() -> void:\n\tvar passed := 0\n\tvar failed := 0\n\tfor file in _test_files():\n\t\tvar script: GDScript = load(TEST_DIR.path_join(file))\n\t\tif script == null or not script.can_instantiate():\n\t\t\tfailed += 1\n\t\t\tprint(\"FAIL %s: script did not load (parse error?)\" % file)\n\t\t\tcontinue\n\t\tfor method in script.get_script_method_list():\n\t\t\tvar name: String = method[\"name\"]\n\t\t\tif not name.begins_with(\"test_\"):\n\t\t\t\tcontinue\n\t\t\tvar suite: Object = script.new()\n\t\t\tsuite.set(\"t\", self)\n\t\t\tif suite.has_method(\"before_each\"):\n\t\t\t\tawait suite.before_each()\n\t\t\t_failures.clear()\n\t\t\t_errors.take()\n\t\t\t_current = \"%s::%s\" % [file.get_basename(), name]\n\t\t\tawait suite.call(name)\n\t\t\tif suite.has_method(\"after_each\"):\n\t\t\t\tawait suite.after_each()\n\t\t\tfor e in _errors.take():\n\t\t\t\t_failures.append(\"script error: \" + e)\n\t\t\tif suite is Node and is_instance_valid(suite):\n\t\t\t\tsuite.free()\n\t\t\tif _failures.is_empty():\n\t\t\t\tpassed += 1\n\t\t\t\tprint(\"PASS %s\" % _current)\n\t\t\telse:\n\t\t\t\tfailed += 1\n\t\t\t\tprint(\"FAIL %s: %s\" % [_current, \"; \".join(_failures)])\n\tprint(\"%d passed, %d failed\" % [passed, failed])\n\tquit(1 if failed > 0 else 0)\n\n\nfunc _test_files() -> PackedStringArray:\n\tvar files := PackedStringArray()\n\tfor f in DirAccess.get_files_at(TEST_DIR):\n\t\tif f.begins_with(\"test_\") and f.ends_with(\".gd\"):\n\t\t\tfiles.append(f)\n\tfiles.sort()\n\treturn files\n\n\n## Assertions, called from tests as t.check(...) and t.equal(...).\nfunc check(condition: bool, message := \"expected true\") -> void:\n\tif not condition:\n\t\t_failures.append(message)\n\n\nfunc equal(actual: Variant, expected: Variant, message := \"\") -> void:\n\tif actual != expected:\n\t\t_failures.append(\"%sexpected %s, got %s\" % [\"\" if message.is_empty() else message + \": \", var_to_str(expected), var_to_str(actual)])\n\n\n## Wait for n frames of the real main loop (process and physics run headless too).\nfunc frames(n := 1) -> void:\n\tfor i in n:\n\t\tawait process_frame\n"
    },
    {
      "path": "skills/godot-headless-tests/scripts/gd_test_lint.py",
      "role": "reference",
      "description": "Read-only linter for test suites that can't fail or that block the run.",
      "skill": "godot-headless-tests",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/godot-headless-tests/scripts/gd_test_lint.py",
        "codex": ".agents/skills/godot-headless-tests/scripts/gd_test_lint.py",
        "cursor": ".agents/skills/godot-headless-tests/scripts/gd_test_lint.py",
        "gemini-cli": ".agents/skills/godot-headless-tests/scripts/gd_test_lint.py",
        "antigravity-cli": ".agents/skills/godot-headless-tests/scripts/gd_test_lint.py",
        "copilot": ".agents/skills/godot-headless-tests/scripts/gd_test_lint.py"
      },
      "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/scripts/gd_test_lint.py",
      "raw_latest_url": "https://setupbay.com/raw/godot-4-headless-tests/latest/skills/godot-headless-tests/scripts/gd_test_lint.py",
      "bytes": 4402,
      "lang": "markdown",
      "content": "#!/usr/bin/env python3\n\"\"\"Read-only linter for Godot 4 headless test suites (tests/test_*.gd).\n\nFinds tests that can't fail, tests that block or end the run, and timing that makes\nheadless runs slow or flaky. Prints findings and exits 1 if there are errors.\n\nUsage: python3 gd_test_lint.py [path ...]   (default: tests)\nA line ending in \"# lint: ok\" is skipped.\n\"\"\"\nimport re\nimport sys\nfrom pathlib import Path\n\nASSERT = re.compile(r\"\\bt\\.(check|equal|fail)\\s*\\(\")\nLINE_RULES = [\n    # (level, id, pattern, advice)\n    (\"error\", \"get-tree-in-suite\", re.compile(r\"(?<![.\\w])get_tree\\s*\\(\"),\n     \"a RefCounted suite isn't in the tree; use t (the runner's SceneTree), e.g. t.root.add_child()\"),\n    (\"error\", \"blocking-delay\", re.compile(r\"\\bOS\\.delay_(msec|usec)\\s*\\(\"),\n     \"blocks the main loop, so no frames run; await t.frames(n) instead\"),\n    (\"error\", \"frames-without-await\", re.compile(r\"^(?!.*\\bawait\\b).*\\bt\\.frames\\s*\\(\"),\n     \"t.frames() is a coroutine; without await the test carries on at once\"),\n    (\"error\", \"quit-in-test\", re.compile(r\"(\\bt\\.|get_tree\\(\\)\\.)quit\\s*\\(\"),\n     \"ends the whole run early and hides the remaining results; let the runner quit\"),\n    (\"error\", \"editor-only-api\", re.compile(r\"\\b(EditorInterface|EditorPlugin|EditorScript)\\b\"),\n     \"the EditorInterface singleton and editor plugins are not available in a --script run; test the game code, not the plugin UI\"),\n    (\"warn\", \"wall-clock-timer\", re.compile(r\"\\bcreate_timer\\s*\\(\"),\n     \"real-time waits make headless runs slow and flaky; call the timeout handler or set Timer.wait_time tiny\"),\n    (\"warn\", \"unseeded-random\", re.compile(r\"(?<![.\\w])(randi|randf|randi_range|randf_range)\\s*\\(\"),\n     \"global RNG differs per run; use RandomNumberGenerator with a fixed seed\"),\n    (\"warn\", \"print-as-check\", re.compile(r\"(?<![.\\w])print\\s*\\(\"),\n     \"a printed value is not a check; use t.equal() so the run fails\"),\n    (\"warn\", \"bare-assert\", re.compile(r\"(?<![.\\w])assert\\s*\\(\"),\n     \"assert() is stripped from release builds and gives no expected/actual; use t.check() or t.equal()\"),\n]\n\n\ndef strip_comment(line: str) -> str:\n    out, quote = [], \"\"\n    for ch in line:\n        if quote:\n            out.append(ch)\n            if ch == quote:\n                quote = \"\"\n        elif ch in \"\\\"'\":\n            quote = ch\n            out.append(ch)\n        elif ch == \"#\":\n            break\n        else:\n            out.append(ch)\n    return \"\".join(out)\n\n\ndef lint(path: Path):\n    findings = []\n    lines = path.read_text(encoding=\"utf-8\", errors=\"replace\").splitlines()\n    test_name, test_line, has_assert = None, 0, False\n\n    def close_test():\n        if test_name and not has_assert:\n            findings.append((\"error\", \"no-assertion\", test_line,\n                             f\"{test_name}() has no t.check/t.equal/t.fail, so it can only pass\"))\n\n    for n, raw in enumerate(lines, 1):\n        if raw.rstrip().endswith(\"# lint: ok\"):\n            continue\n        code = strip_comment(raw)\n        if not code.strip():\n            continue\n        m = re.match(r\"^func\\s+(\\w+)\\s*\\(\", code)\n        if m or re.match(r\"^\\S\", code):\n            close_test()\n            test_name = m.group(1) if m and m.group(1).startswith(\"test_\") else None\n            test_line, has_assert = n, False\n        if test_name and ASSERT.search(code):\n            has_assert = True\n        for level, rid, pat, advice in LINE_RULES:\n            if pat.search(code):\n                findings.append((level, rid, n, advice))\n    close_test()\n    return sorted(findings, key=lambda f: f[2])\n\n\ndef main(argv):\n    targets = [Path(a) for a in argv] or [Path(\"tests\")]\n    files = []\n    for t in targets:\n        if t.is_dir():\n            files += sorted(p for p in t.rglob(\"test_*.gd\") if \".godot\" not in p.parts)\n        elif t.is_file():\n            files.append(t)\n        else:\n            print(f\"gd_test_lint: no such path: {t}\", file=sys.stderr)\n            return 2\n    errors = warnings = 0\n    for f in files:\n        for level, rid, line, advice in lint(f):\n            print(f\"{f}:{line}: {level} [{rid}] {advice}\")\n            if level == \"error\":\n                errors += 1\n            else:\n                warnings += 1\n    print(f\"gd_test_lint: {len(files)} file(s), {errors} error(s), {warnings} warning(s)\")\n    return 1 if errors else 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main(sys.argv[1:]))\n"
    },
    {
      "path": "skills/godot-headless-tests/references/bad-test-sample.gd.txt",
      "role": "reference",
      "description": "A suite with the usual mistakes, for checking the linter.",
      "skill": "godot-headless-tests",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
        "codex": ".agents/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
        "cursor": ".agents/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
        "gemini-cli": ".agents/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
        "antigravity-cli": ".agents/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
        "copilot": ".agents/skills/godot-headless-tests/references/bad-test-sample.gd.txt"
      },
      "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
      "raw_latest_url": "https://setupbay.com/raw/godot-4-headless-tests/latest/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
      "bytes": 793,
      "lang": "markdown",
      "content": "extends RefCounted\n## A suite with the usual headless-test mistakes. Copy it to test_bad.gd to try the linter.\n\nvar t: SceneTree\nvar player: Node\n\n\nfunc before_each() -> void:\n\tplayer = load(\"res://scenes/player.tscn\").instantiate()\n\tget_tree().root.add_child(player)\n\n\nfunc test_spawns() -> void:\n\tprint(player.position)\n\n\nfunc test_dash_cooldown() -> void:\n\tplayer.dash()\n\tOS.delay_msec(500)\n\tt.equal(player.can_dash, true)\n\n\nfunc test_respawn() -> void:\n\tplayer.die()\n\tawait t.create_timer(2.0).timeout\n\tt.frames(1)\n\tt.check(player.is_alive())\n\n\nfunc test_loot_roll() -> void:\n\tvar roll := randi_range(1, 6)\n\tassert(roll >= 1)\n\n\nfunc test_editor_gizmo() -> void:\n\tvar ei = EditorInterface.get_selection()\n\tt.check(ei != null)\n\n\nfunc test_last() -> void:\n\tt.equal(player.lives, 3)\n\tt.quit()\n"
    }
  ],
  "requires": {
    "tools": [
      "godot-4",
      "python3"
    ],
    "env": [],
    "mcp": []
  },
  "tested": {
    "status": "untested",
    "last_run": null,
    "fixture": null,
    "per_agent": {
      "claude-code": "untested",
      "codex": "untested",
      "cursor": "untested",
      "gemini-cli": "untested",
      "antigravity-cli": "untested",
      "copilot": "untested"
    }
  },
  "security": {
    "scanned": true,
    "scanner": "intake-precheck/2",
    "verdict": "clean",
    "network": false,
    "scripts": [
      "gd_test_lint.py",
      "run_tests.gd"
    ],
    "notes": "The linter only reads .gd files and prints findings. The runner only loads the project's own tests/test_*.gd files inside Godot; neither edits files or makes network calls."
  },
  "featured": false,
  "deprecated": false,
  "last_checked": "2026-10-29",
  "published": "2026-10-29",
  "updated": "2026-10-29",
  "compatibility": {
    "claude-code": "native",
    "codex": "native",
    "cursor": "native",
    "gemini-cli": "native",
    "antigravity-cli": "native",
    "copilot": "native"
  },
  "counts": {
    "rules": 2,
    "skills": 1,
    "mcp": 0
  },
  "url": "https://setupbay.com/loadouts/godot-4-headless-tests/",
  "api_url": "https://setupbay.com/api/loadouts/godot-4-headless-tests.json",
  "verification": {
    "level": "verified-install",
    "status": "verified",
    "label": "Verified install",
    "verified": true,
    "checked_at": "2026-10-29T12:58:14Z",
    "commit": "cc18d7ef3e9c98d99f00c17e2ef9a0565a3f9597",
    "content_current": true,
    "agent_tests": "not-run",
    "counts": {
      "pass": 9,
      "fail": 0,
      "skip": 0,
      "not_applicable": 1
    },
    "meaning": "Level 1: automated checks on the files (parse, install paths, MCP config, safety scan, references, size, licence, starter build). No AI agent was run.",
    "checks": [
      {
        "id": "listing",
        "name": "Listing",
        "status": "pass",
        "summary": "Schema valid; 8 files declared, none missing or extra",
        "details": [
          "tested.status: untested (agent tests are separate from Level 1)"
        ]
      },
      {
        "id": "parse",
        "name": "Files parse",
        "status": "pass",
        "summary": "All 8 files parse; front matter valid",
        "details": []
      },
      {
        "id": "adapters",
        "name": "Adapters",
        "status": "pass",
        "summary": "CLAUDE.md and GEMINI.md import AGENTS.md and are up to date",
        "details": [
          "CLAUDE.md: matches the generator",
          "GEMINI.md: matches the generator"
        ]
      },
      {
        "id": "install-paths",
        "name": "Install paths",
        "status": "pass",
        "summary": "Correct paths for all 6 agents; layouts re-discovered",
        "details": [
          "Claude Code: 6 files, 1 skill in .claude/skills/",
          "Codex CLI: 5 files, 1 skill in .agents/skills/",
          "Cursor: 6 files, 1 skill in .agents/skills/, 1 rule",
          "Gemini CLI: 6 files, 1 skill in .agents/skills/",
          "Antigravity CLI: 5 files, 1 skill in .agents/skills/",
          "GitHub Copilot: 5 files, 1 skill in .agents/skills/"
        ]
      },
      {
        "id": "mcp",
        "name": "MCP config",
        "status": "n/a",
        "summary": "No MCP servers in this loadout",
        "details": []
      },
      {
        "id": "safety",
        "name": "Safety scan",
        "status": "pass",
        "summary": "Clean: no credentials, risky shell, hidden text or covert instructions",
        "details": [
          "Scanner intake-precheck/2: verdict clean; network no (listing says no)"
        ]
      },
      {
        "id": "references",
        "name": "References",
        "status": "pass",
        "summary": "1 reference between files resolves for every agent",
        "details": []
      },
      {
        "id": "size",
        "name": "Size limits",
        "status": "pass",
        "summary": "8 files, 15.9 KB total; largest skills/godot-headless-tests/scripts/gd_test_lint.py 4.3 KB",
        "details": [
          "Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule"
        ]
      },
      {
        "id": "licence",
        "name": "Licence",
        "status": "pass",
        "summary": "MIT, by SetupBay; text in the repository LICENSE",
        "details": []
      },
      {
        "id": "starter-build",
        "name": "Starter build",
        "status": "pass",
        "summary": "A Godot 4.7.2 project with a Health component, an arena scene and a GameState autoload. gd_test_lint.py passes its suites and flags the skill's bad sample with 7 errors and 4 warnings. Run headless, the skill's runner first fails on a fresh clone with no import cache, then passes all 7 tests after -",
        "details": [
          "Applied 12 loadout files at every agent's paths",
          "godot 4.7.2-stable",
          "✓ place the skill's runner: bash -c cp .agents/skills/godot-headless-tests/references/run_tests.gd tests/run_tests.gd && echo placed",
          "✓ lint: the project's suites are clean: bash -c python3 .agents/skills/godot-headless-tests/scripts/gd_test_lint.py tests; echo \"exit=$?\"",
          "✓ lint: the bad sample is flagged line by line: bash -c mkdir -p bad && cp .claude/skills/godot-headless-tests/references/bad-test-sample.gd.txt bad/test_bad.gd && python3 .claude/skills/godot-headless-tests/scripts/gd_test_lint.py bad; echo \"exit=$?\"",
          "✓ fresh clone without --import: class_name isn't known yet, and the run fails: bash -c rm -rf .godot && \"$GODOT_BIN\" --headless --path . --script res://tests/run_tests.gd 2>&1; echo \"exit=$?\"",
          "✓ build the import cache: bash -c \"$GODOT_BIN\" --headless --import --path . >/dev/null 2>&1; echo \"import=$?\"; test -f .godot/global_script_class_cache.cfg && echo cache-ok",
          "✓ run: every test passes, exit 0: bash -c \"$GODOT_BIN\" --headless --path . --script res://tests/run_tests.gd 2>&1; echo \"exit=$?\"",
          "✓ a broken damage rule fails the run: bash -c cp scripts/health.gd health.bak && sed -i 's/current = maxi(current - amount, 0)/current -= amount/' scripts/health.gd && \"$GODOT_BIN\" --headless --path . --script res://tests/run_tests.gd 2>&1; echo \"exit=$?\"; mv health.bak scripts/health.gd",
          "✓ a script error inside a test is a failure, not a silent pass: bash -c printf 'extends RefCounted\\nvar t: SceneTree\\n\\nfunc test_null_enemy() -> void:\\n\\tvar enemy: Node = null\\n\\tenemy.get_name()\\n\\tt.check(true)\\n' > tests/test_null.gd && \"$GODOT_BIN\" --headless --path . --script res://tests/run_tests.gd 2>&1; echo \"exit=$?\"; sed 's/^\\tOS.add_logger(_errors)$/\\tpass/' tests/run_tests.gd > tests/naive_runner.gd && echo '--- without the Logger hook ---' && \"$GODOT_BIN\" --headless --path . --scr",
          "Covers: The runner from the skill under Godot 4.7.2 headless: passing, failing on a broken rule, and failing on a script error",
          "Covers: The fresh-clone trap: class_name types are unknown until --import builds .godot/",
          "Covers: Scene tests with real frames, queue_free timing and an autoload reset between tests",
          "Covers: The linter on clean suites and on every rule in the bad sample",
          "Covers: Not covered: rendering, audio, input devices and physics feel, which need a human play-test"
        ]
      }
    ]
  },
  "install": {
    "claude-code": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "CLAUDE.md",
          "target": "CLAUDE.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/CLAUDE.md",
          "role": "adapter"
        },
        {
          "path": "skills/godot-headless-tests/SKILL.md",
          "target": ".claude/skills/godot-headless-tests/SKILL.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/godot-headless-tests/references/run_tests.gd",
          "target": ".claude/skills/godot-headless-tests/references/run_tests.gd",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/run_tests.gd",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/scripts/gd_test_lint.py",
          "target": ".claude/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "target": ".claude/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://setupbay.com/download/godot-4-headless-tests/claude-code.zip"
    },
    "codex": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/godot-headless-tests/SKILL.md",
          "target": ".agents/skills/godot-headless-tests/SKILL.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/godot-headless-tests/references/run_tests.gd",
          "target": ".agents/skills/godot-headless-tests/references/run_tests.gd",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/run_tests.gd",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/scripts/gd_test_lint.py",
          "target": ".agents/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "target": ".agents/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://setupbay.com/download/godot-4-headless-tests/codex.zip"
    },
    "cursor": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": ".cursor/rules/godot-headless-tests.mdc",
          "target": ".cursor/rules/godot-headless-tests.mdc",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/.cursor/rules/godot-headless-tests.mdc",
          "role": "rules"
        },
        {
          "path": "skills/godot-headless-tests/SKILL.md",
          "target": ".agents/skills/godot-headless-tests/SKILL.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/godot-headless-tests/references/run_tests.gd",
          "target": ".agents/skills/godot-headless-tests/references/run_tests.gd",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/run_tests.gd",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/scripts/gd_test_lint.py",
          "target": ".agents/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "target": ".agents/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://setupbay.com/download/godot-4-headless-tests/cursor.zip"
    },
    "gemini-cli": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "GEMINI.md",
          "target": "GEMINI.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/GEMINI.md",
          "role": "adapter"
        },
        {
          "path": "skills/godot-headless-tests/SKILL.md",
          "target": ".agents/skills/godot-headless-tests/SKILL.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/godot-headless-tests/references/run_tests.gd",
          "target": ".agents/skills/godot-headless-tests/references/run_tests.gd",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/run_tests.gd",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/scripts/gd_test_lint.py",
          "target": ".agents/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "target": ".agents/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://setupbay.com/download/godot-4-headless-tests/gemini-cli.zip"
    },
    "antigravity-cli": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/godot-headless-tests/SKILL.md",
          "target": ".agents/skills/godot-headless-tests/SKILL.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/godot-headless-tests/references/run_tests.gd",
          "target": ".agents/skills/godot-headless-tests/references/run_tests.gd",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/run_tests.gd",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/scripts/gd_test_lint.py",
          "target": ".agents/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "target": ".agents/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://setupbay.com/download/godot-4-headless-tests/antigravity-cli.zip"
    },
    "copilot": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/godot-headless-tests/SKILL.md",
          "target": ".agents/skills/godot-headless-tests/SKILL.md",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/godot-headless-tests/references/run_tests.gd",
          "target": ".agents/skills/godot-headless-tests/references/run_tests.gd",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/run_tests.gd",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/scripts/gd_test_lint.py",
          "target": ".agents/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/scripts/gd_test_lint.py",
          "role": "reference"
        },
        {
          "path": "skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "target": ".agents/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "raw_url": "https://setupbay.com/raw/godot-4-headless-tests/1.0.0/skills/godot-headless-tests/references/bad-test-sample.gd.txt",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://setupbay.com/download/godot-4-headless-tests/copilot.zip"
    }
  },
  "source_zip_url": "https://setupbay.com/download/godot-4-headless-tests/source.zip",
  "claude_code_plugin": {
    "name": "godot-4-headless-tests",
    "marketplace_url": "https://setupbay.com/marketplace.json",
    "install": [
      "/plugin marketplace add https://setupbay.com/marketplace.json",
      "/plugin install godot-4-headless-tests@setupbay"
    ],
    "zip_url": "https://setupbay.com/plugins/godot-4-headless-tests.zip",
    "sha256": "b83069d40d916caa84b863f24b73f208af3d349713e6fe48b7607a75743c2537",
    "carries": [
      "skills"
    ],
    "note": "Plugins do not carry project rules (AGENTS.md, CLAUDE.md); install those from install.claude-code."
  }
}