diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 46c2e79..c316bc9 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3865,4 +3865,13 @@ target_link_libraries(step553_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step554_test tests/step554_test.cpp) +target_include_directories(step554_test PRIVATE src) +target_link_libraries(step554_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/DebugSessionModel.h b/editor/src/DebugSessionModel.h new file mode 100644 index 0000000..46e0909 --- /dev/null +++ b/editor/src/DebugSessionModel.h @@ -0,0 +1,106 @@ +#pragma once +// Step 554: Debug Session Model + +#include +#include +#include + +struct DebugTargetContext { + std::string targetId; + std::string bufferId; + std::string executablePath; + bool attached = false; +}; + +struct DebugSessionState { + std::string sessionId; + bool active = false; + bool paused = false; + std::map targets; + std::string currentTargetId; +}; + +class DebugSessionModel { +public: + DebugSessionModel() { + state_.sessionId = "debug-session-1"; + } + + const DebugSessionState& state() const { return state_; } + + bool start(const std::string& targetId, + const std::string& bufferId, + const std::string& executablePath) { + if (state_.active) return false; + state_.active = true; + state_.paused = false; + state_.targets.clear(); + state_.currentTargetId.clear(); + return addTargetInternal(targetId, bufferId, executablePath, false, true); + } + + bool attach(const std::string& targetId, + const std::string& bufferId, + const std::string& executablePath) { + if (!state_.active) state_.active = true; + return addTargetInternal(targetId, bufferId, executablePath, true, + state_.currentTargetId.empty()); + } + + bool pause() { + if (!state_.active || state_.paused) return false; + state_.paused = true; + return true; + } + + bool resume() { + if (!state_.active || !state_.paused) return false; + state_.paused = false; + return true; + } + + bool stop() { + if (!state_.active) return false; + state_.active = false; + state_.paused = false; + state_.targets.clear(); + state_.currentTargetId.clear(); + return true; + } + + bool switchTarget(const std::string& targetId) { + if (!state_.active) return false; + if (state_.targets.count(targetId) == 0) return false; + state_.currentTargetId = targetId; + return true; + } + + bool hasTargetForBuffer(const std::string& bufferId) const { + for (const auto& kv : state_.targets) { + if (kv.second.bufferId == bufferId) return true; + } + return false; + } + +private: + DebugSessionState state_; + + bool addTargetInternal(const std::string& targetId, + const std::string& bufferId, + const std::string& executablePath, + bool attached, + bool makeCurrent) { + if (targetId.empty() || bufferId.empty() || executablePath.empty()) return false; + if (state_.targets.count(targetId) != 0) return false; + + DebugTargetContext context; + context.targetId = targetId; + context.bufferId = bufferId; + context.executablePath = executablePath; + context.attached = attached; + state_.targets[targetId] = context; + + if (makeCurrent) state_.currentTargetId = targetId; + return true; + } +}; diff --git a/editor/tests/step554_test.cpp b/editor/tests/step554_test.cpp new file mode 100644 index 0000000..3632cfd --- /dev/null +++ b/editor/tests/step554_test.cpp @@ -0,0 +1,140 @@ +// Step 554: Debug Session Model (12 tests) + +#include "DebugSessionModel.h" + +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +void test_start_creates_active_session() { + TEST(start_creates_active_session); + DebugSessionModel m; + CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed"); + CHECK(m.state().active, "session should be active"); + CHECK(!m.state().paused, "session should not be paused"); + PASS(); +} + +void test_start_fails_when_already_active() { + TEST(start_fails_when_already_active); + DebugSessionModel m; + CHECK(m.start("t1", "buf1", "/bin/app"), "first start should succeed"); + CHECK(!m.start("t2", "buf2", "/bin/app2"), "second start should fail"); + PASS(); +} + +void test_attach_starts_session_when_inactive() { + TEST(attach_starts_session_when_inactive); + DebugSessionModel m; + CHECK(m.attach("t1", "buf1", "/bin/app"), "attach should start inactive session"); + CHECK(m.state().active, "session should become active"); + CHECK(m.state().targets.at("t1").attached, "target should be attached"); + PASS(); +} + +void test_attach_adds_secondary_target() { + TEST(attach_adds_secondary_target); + DebugSessionModel m; + CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed"); + CHECK(m.attach("t2", "buf2", "/bin/app2"), "attach should add second target"); + CHECK(m.state().targets.size() == 2, "two targets expected"); + PASS(); +} + +void test_pause_transitions_to_paused() { + TEST(pause_transitions_to_paused); + DebugSessionModel m; + CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed"); + CHECK(m.pause(), "pause should succeed"); + CHECK(m.state().paused, "session should be paused"); + PASS(); +} + +void test_resume_transitions_from_paused() { + TEST(resume_transitions_from_paused); + DebugSessionModel m; + CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed"); + CHECK(m.pause(), "pause should succeed"); + CHECK(m.resume(), "resume should succeed"); + CHECK(!m.state().paused, "session should not be paused"); + PASS(); +} + +void test_pause_fails_when_inactive() { + TEST(pause_fails_when_inactive); + DebugSessionModel m; + CHECK(!m.pause(), "pause should fail when inactive"); + PASS(); +} + +void test_resume_fails_when_not_paused() { + TEST(resume_fails_when_not_paused); + DebugSessionModel m; + CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed"); + CHECK(!m.resume(), "resume should fail when not paused"); + PASS(); +} + +void test_stop_clears_session_and_targets() { + TEST(stop_clears_session_and_targets); + DebugSessionModel m; + CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed"); + CHECK(m.attach("t2", "buf2", "/bin/app2"), "attach should succeed"); + CHECK(m.stop(), "stop should succeed"); + CHECK(!m.state().active, "session should be inactive"); + CHECK(m.state().targets.empty(), "targets should be cleared"); + PASS(); +} + +void test_switch_target_changes_current_target() { + TEST(switch_target_changes_current_target); + DebugSessionModel m; + CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed"); + CHECK(m.attach("t2", "buf2", "/bin/app2"), "attach should succeed"); + CHECK(m.switchTarget("t2"), "switch should succeed"); + CHECK(m.state().currentTargetId == "t2", "current target mismatch"); + PASS(); +} + +void test_switch_target_fails_for_unknown_target() { + TEST(switch_target_fails_for_unknown_target); + DebugSessionModel m; + CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed"); + CHECK(!m.switchTarget("missing"), "switch should fail for unknown target"); + PASS(); +} + +void test_has_target_for_buffer_maps_per_buffer_context() { + TEST(has_target_for_buffer_maps_per_buffer_context); + DebugSessionModel m; + CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed"); + CHECK(m.attach("t2", "buf2", "/bin/app2"), "attach should succeed"); + CHECK(m.hasTargetForBuffer("buf1"), "buffer1 should map to target"); + CHECK(m.hasTargetForBuffer("buf2"), "buffer2 should map to target"); + CHECK(!m.hasTargetForBuffer("buf3"), "unknown buffer should not map"); + PASS(); +} + +int main() { + std::cout << "Step 554: Debug Session Model\n"; + + test_start_creates_active_session(); // 1 + test_start_fails_when_already_active(); // 2 + test_attach_starts_session_when_inactive(); // 3 + test_attach_adds_secondary_target(); // 4 + test_pause_transitions_to_paused(); // 5 + test_resume_transitions_from_paused(); // 6 + test_pause_fails_when_inactive(); // 7 + test_resume_fails_when_not_paused(); // 8 + test_stop_clears_session_and_targets(); // 9 + test_switch_target_changes_current_target(); // 10 + test_switch_target_fails_for_unknown_target(); // 11 + test_has_target_for_buffer_maps_per_buffer_context(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 7aa3aa1..9204406 100644 --- a/progress.md +++ b/progress.md @@ -9769,3 +9769,36 @@ duplication and re-validate architecture constraints without behavior changes. - `./editor/build-native/step551_test` - PASS (12/12) - `./editor/build-native/step552_test` - PASS (12/12) - `./editor/build-native/step553_test` - PASS (8/8) + +### Step 554: Debug Session Model +**Status:** PASS (12/12 tests) + +Implements the core debug session lifecycle model with start/attach/pause/ +resume/stop transitions and multi-target per-buffer context support. + +**Files added:** +- `editor/src/DebugSessionModel.h` - debug session control-plane model: + - session lifecycle transitions (`start/attach/pause/resume/stop`) + - multi-target session state with per-target buffer/executable context + - current-target switching + - per-buffer target lookup for editor mapping +- `editor/tests/step554_test.cpp` - 12 tests covering: + - lifecycle transition behavior and invalid transition guards + - attach semantics for inactive/active sessions + - multi-target session behavior + - target switching behavior + - per-buffer target mapping behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step554_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step554_test step553_test` - PASS +- `./editor/build-native/step554_test` - PASS (12/12) +- `./editor/build-native/step553_test` - PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/DebugSessionModel.h` within header-size limit (`106` <= `600`) +- `editor/tests/step554_test.cpp` within test-file size guidance (`140` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`