Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,27 @@ jobs:
$env:PYTHONPATH = Join-Path $PWD 'tests'
python -m unittest tests.test_managed_recovery_live tests.test_managed_recovery_adversarial -v

# An editor opens its startup scenes once its first scan is applied, and
# a scene opened before then was replaced (#1069). The integration
# harness cannot see it: its fixture plugin opens a scene of its own once
# startup is over, so this launches a fresh editor and opens one at once.
- name: Verify a scene opened at editor startup stays current
shell: pwsh
env:
GODOT_VERSION: ${{ matrix.godot-version }}
run: |
$godot = Get-ChildItem -Path godot-bin -Filter "Godot_v${env:GODOT_VERSION}-stable_win64_console.exe" |
Select-Object -First 1
if (-not $godot) {
$godot = Get-ChildItem -Path godot-bin -Filter "Godot_v${env:GODOT_VERSION}-stable_win64*.exe" |
Where-Object { $_.Name -notlike "*console*" } | Select-Object -First 1
}
if (-not $godot) { throw "Godot executable not found under godot-bin" }
$env:DIDI_TEST_BINARY = (Resolve-Path build/didi.exe).Path
$env:DIDI_STARTUP_GODOT = $godot.FullName
$env:PYTHONPATH = Join-Path $PWD 'tests'
python -m unittest tests.test_editor_startup_live -v

# tests/contract_snapshots/live-<line>.json: the listings once an editor
# on this line is attached, and the answers to the read-only calls in
# calls.json against tests/contract_fixture (Q3). It runs whenever the
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,31 @@ The three Phase 7 blockers are unchanged; the newest name is `asset_configure_im

### Fixed

- **`runtime_launch` with a Godot that cannot be started says so (#1076).** It
answered `success: false` with `exit_code: 0`, which is what a game that ran
and failed looks like, and said to put godot on PATH whatever `GODOT_BIN`
named. It now answers `503 engine_unavailable`, like the other tools that
start their own Godot.

- **`csharp_check_build` no longer calls a slow .NET SDK missing (#1078).**
Its `dotnet --version` probe stopped at a fixed 30 seconds, whatever
`timeout_seconds` said, and then told the caller to install the SDK. The
probe now runs under the call's `timeout_seconds`, and running out of it is
`504 timeout`, retryable.

- **`scene_create` over a scene open in another tab opens it on Godot 4.5 and
4.6 (#1079, #1073).** It rebuilt the tab and then opened it in one request,
and the editor ignores a scene change for the rest of that frame, so it
answered `opened: false`. The tab is now brought to the front first and
rebuilt on a later request, through the reload the other writers use, and
the answer carries `editor_scene_reloaded`. `previous_scene_file_path` now
names the scene that was current before the call.

- **A scene opened straight after an editor starts stays the edited scene
(#1069).** `scene_open` and `scene_create` now wait until the editor has
opened its startup scenes, which it does once its first scan is applied.
Before, the startup made the main scene current a moment after the answer.

- **A pack over a scene open in another tab no longer comes undone on the next
save (#1072).** `scene_pack_branch` with `overwrite: true` left that tab
holding the old tree, and `editor_save_scene` wrote it back over the pack.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![CI](https://github.com/saworbit/didi/actions/workflows/ci.yml/badge.svg)](https://github.com/saworbit/didi/actions/workflows/ci.yml)
[![CodeQL](https://github.com/saworbit/didi/actions/workflows/codeql.yml/badge.svg)](https://github.com/saworbit/didi/actions/workflows/codeql.yml)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/saworbit/didi/badge)](https://scorecard.dev/viewer/?uri=github.com/saworbit/didi)
[![Tests](https://img.shields.io/badge/tests-1493-2ea043?logo=pytest&logoColor=white)](docs/TEST_INVENTORY.md)
[![Tests](https://img.shields.io/badge/tests-1498-2ea043?logo=pytest&logoColor=white)](docs/TEST_INVENTORY.md)
[![Release](https://img.shields.io/github/v/release/saworbit/didi?logo=github&color=blue)](https://github.com/saworbit/didi/releases/latest)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Godot Engine](https://img.shields.io/badge/Godot-4.5%2B-478cbf?logo=godotengine&logoColor=white)](https://godotengine.org/)
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Godot's `SceneTree`, `EditorInterface`, and `RenderingServer` are **not thread-s
```

### Key Safety Guarantees:
1. **Main-Thread Godot Calls**: Supported live scene and viewport operations run only after the native main-loop callback drains the synchronized queue. The engine can run that callback from inside one of its own calls: `EditorFileSystem.reimport_files` and `RenderingServer.force_draw` do, and so does every editor import pass, because a windowed editor's progress dialog pumps the main loop. Such a frame only advances the multi-frame work already in flight (runtime steps, reimports, profiler windows, invariant watches, scene exploration, captures, script calls) and takes nothing off the queue, so no command runs against a tree that is mid-reimport or mid-capture. The pass is open from `resources_reimporting` to `resources_reimported`, which the addon's `didi_import_watch.gd` counts, and on Godot 4.7 also while `EditorFileSystem.is_importing()` is true (#914). Applying a scan is not an import pass. A scan clears the editor's scanning flag on its own thread, and a later frame swaps in the new file index and registers new scripts' classes and documentation under progress tasks that pump the main loop, before it emits `sources_changed`. `asset_reimport` neither reimports nor answers after a scan it started until that signal, which the same addon script counts, has fired for a new index (#994). The queue itself is still open in those frames, so a command from another caller, or one sent during a scan the editor started, can run inside them (#995).
1. **Main-Thread Godot Calls**: Supported live scene and viewport operations run only after the native main-loop callback drains the synchronized queue. The engine can run that callback from inside one of its own calls: `EditorFileSystem.reimport_files` and `RenderingServer.force_draw` do, and so does every editor import pass, because a windowed editor's progress dialog pumps the main loop. Such a frame only advances the multi-frame work already in flight (runtime steps, reimports, profiler windows, invariant watches, scene exploration, captures, script calls) and takes nothing off the queue, so no command runs against a tree that is mid-reimport or mid-capture. The pass is open from `resources_reimporting` to `resources_reimported`, which the addon's `didi_import_watch.gd` counts, and on Godot 4.7 also while `EditorFileSystem.is_importing()` is true (#914). Applying a scan is not an import pass. A scan clears the editor's scanning flag on its own thread, and a later frame swaps in the new file index and registers new scripts' classes and documentation under progress tasks that pump the main loop, before it emits `sources_changed`. `asset_reimport` neither reimports nor answers after a scan it started until that signal, which the same addon script counts, has fired for a new index (#994). Those tasks pump the main loop through the editor's progress dialog, and a frame with a progress task open is held the same way, whoever started the work (#995). One more hold is not about a nested frame. The editor opens the scenes it restores, or the project's main scene, once its first scan is applied, and makes one of them current; `scene.open` and `scene.create` wait at the front of the queue until then, with everything behind them, because a scene they opened earlier was replaced a moment later (#1069). The editor's file index is empty until that scan is applied, which is how the hook tells.
2. **Editor Undo/Redo Integration**: All modifications register transactions with Godot's `EditorUndoRedoManager`, allowing human developers to press `Ctrl+Z` in the editor to undo any AI-generated modification.
3. **Timeout & Deadlock Protection**:
- IPC client operations use recursive mutexes and platform-specific readiness checks with millisecond deadlines: `PeekNamedPipe` on Windows and `poll` on POSIX.
Expand Down
2 changes: 1 addition & 1 deletion docs/LLM_INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ When live, use focused tools:
- Send `{"x": .., "y": ..}` or `{"x": .., "y": .., "z": ..}` for a Vector2/Vector3, whole numbers for the integer versions, `{"r": .., "g": .., "b": ..}` with an optional `a` or a `"#rrggbb"` string for a Color, and a `res://` path for a Resource slot. `null` clears a resource slot. An extra or missing member is refused rather than dropped. The same object shape works for a coordinate in `tilemap_set_cells` and `gridmap_set_cells`, which also still take `[x, y]` and `[x, y, z]`; each of those two takes the other's field name, `coords` or `position`, for the same thing.
- `editor_undo` and `editor_redo` to verify reversibility.
- `editor_save_scene` only when persistence is intended.
- `editor_reload_project` to request a resource-filesystem source rescan. It does not reload a resource the editor already holds, so it is not the fix for an editor answering from an old copy of a file. The tools that write files reload the attached editor's copy themselves: read `editor_copy_reloaded`, or `editor_copies_reloaded` from `project_apply_changes` and `project_rename_references`. `editor_copy_error` or `editor_copy_errors` names a copy that could not be reloaded, such as a script that no longer compiles. A scene those two rewrite that is open in a tab is rebuilt from the file and named in `editor_scenes_reloaded`; one with unsaved changes, or any open one before Godot 4.7, refuses the call until you save it and pass `discard_unsaved: true`. `scene_pack_branch` and `viewport_create_test_lab` with `overwrite: true` rebuild a tab that holds the scene they replace whatever it holds, unsaved changes included, and answer `editor_scene_reloaded: true`.
- `editor_reload_project` to request a resource-filesystem source rescan. It does not reload a resource the editor already holds, so it is not the fix for an editor answering from an old copy of a file. The tools that write files reload the attached editor's copy themselves: read `editor_copy_reloaded`, or `editor_copies_reloaded` from `project_apply_changes` and `project_rename_references`. `editor_copy_error` or `editor_copy_errors` names a copy that could not be reloaded, such as a script that no longer compiles. A scene those two rewrite that is open in a tab is rebuilt from the file and named in `editor_scenes_reloaded`; one with unsaved changes, or any open one before Godot 4.7, refuses the call until you save it and pass `discard_unsaved: true`. `scene_create`, `scene_pack_branch` and `viewport_create_test_lab` with `overwrite: true` rebuild a tab that holds the scene they replace whatever it holds, unsaved changes included, and answer `editor_scene_reloaded: true`.
- `runtime_launch` with `detach: true` to start the game and leave it running. It answers with `game_session`; pass its `session_id` to `runtime_attach_session` and the runtime tools answer on the running game. End it with `runtime_stop`. Without `detach` the call blocks and kills the game at the timeout, which is what you want for a test and not for playing one.

Before executing any implemented mutation, call the exact tool and arguments with `dry_run: true`, inspect `mutation_preview`, and verify the intended project and route. Dry-runs do not enter handlers. If the preview includes `confirmation_token`, repeat the exact original arguments without `dry_run` and add that token only after the destructive intent is authorized. Never combine `dry_run: true` and `confirmation_token`, alter arguments between preview and execution, persist a token, or retry it: tokens are 64 lowercase hex characters, expire after 120 seconds, and are consumed by the mutation they authorise. An attempt refused for not matching its own binding leaves the token usable, so retry it with the arguments you previewed rather than taking a fresh preview.
Expand Down
21 changes: 11 additions & 10 deletions docs/TEST_INVENTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ Every number here is derived from the suites themselves rather than written down

| Measure | Count |
| --- | ---: |
| Automated tests | **1493** |
| Live-harness assertions | 1406 |
| Automated tests | **1498** |
| Live-harness assertions | 1412 |

The badge in the [README](../README.md) shows the automated test total. Harness assertions are counted separately because they are assertions inside one live scenario, not independently runnable cases; adding them together would flatter the number.

## Native C++ suite (`didi_tests`)

**953 tests.** Derived from `didi_tests --list`, which prints the registry the runner iterates.
**957 tests.** Derived from `didi_tests --list`, which prints the registry the runner iterates.

| Suite | Tests |
| --- | ---: |
Expand All @@ -32,7 +32,7 @@ The badge in the [README](../README.md) shows the automated test total. Harness
| `Checkpoints` | 14 |
| `ControlRoom` | 24 |
| `CrashCapture` | 3 |
| `EditorHook` | 9 |
| `EditorHook` | 10 |
| `ElasticIngress` | 4 |
| `EngineDiagnostics` | 7 |
| `ErrorData` | 3 |
Expand All @@ -48,7 +48,7 @@ The badge in the [README](../README.md) shows the automated test total. Harness
| `JsonRpc` | 5 |
| `ManagedProcess` | 6 |
| `McpServer` | 42 |
| `Phase5` | 30 |
| `Phase5` | 31 |
| `Phase6` | 25 |
| `Phase7Contract` | 9 |
| `Phase7Diagnostics` | 1 |
Expand All @@ -65,7 +65,7 @@ The badge in the [README](../README.md) shows the automated test total. Harness
| `ResourceIndexer` | 12 |
| `Resources` | 2 |
| `ResponseEconomy` | 18 |
| `RuntimeLaunch` | 11 |
| `RuntimeLaunch` | 12 |
| `RuntimeLogs` | 12 |
| `RuntimeOutput` | 6 |
| `RuntimeRouting` | 65 |
Expand All @@ -79,7 +79,7 @@ The badge in the [README](../README.md) shows the automated test total. Harness
| `TestRunner` | 2 |
| `ToolManifest` | 1 |
| `ToolProfile` | 6 |
| `Tools` | 169 |
| `Tools` | 170 |
| `UiListControls` | 6 |
| `ViewportIsolation` | 2 |
| `autoload_diagnostics` | 9 |
Expand All @@ -101,7 +101,7 @@ The badge in the [README](../README.md) shows the automated test total. Harness

## Python contract suites (`tests/test_*.py`)

**540 tests.** Derived from `test_*` methods on every `unittest.TestCase` subclass, read with `ast`.
**541 tests.** Derived from `test_*` methods on every `unittest.TestCase` subclass, read with `ast`.

| File | Tests |
| --- | ---: |
Expand All @@ -116,6 +116,7 @@ The badge in the [README](../README.md) shows the automated test total. Harness
| `test_didi_binary.py` | 10 |
| `test_documentation_validator.py` | 104 |
| `test_editor_console.py` | 9 |
| `test_editor_startup_live.py` | 1 |
| `test_elastic_ingress.py` | 13 |
| `test_elicitation_confirmation.py` | 6 |
| `test_engine_identity.py` | 5 |
Expand All @@ -142,7 +143,7 @@ The badge in the [README](../README.md) shows the automated test total. Harness

## Live Godot harness (`tests/*.ps1`)

**1406 assertions.** Derived from `Assert-True` call sites. The harness is one long live scenario rather than a set of named cases, so this counts assertions and says so.
**1412 assertions.** Derived from `Assert-True` call sites. The harness is one long live scenario rather than a set of named cases, so this counts assertions and says so.

| File | Assertions |
| --- | ---: |
Expand All @@ -151,7 +152,7 @@ The badge in the [README](../README.md) shows the automated test total. Harness
| `observed_post_state.ps1` | 12 |
| `refusal_remedies.ps1` | 3 |
| `run_godot_integration.ps1` | 1367 |
| `scene_tab_reload.ps1` | 18 |
| `scene_tab_reload.ps1` | 24 |

## What is not counted here

Expand Down
Loading
Loading