diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index a9619c9..1fc9f84 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4018,4 +4018,13 @@ target_link_libraries(step570_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step571_test tests/step571_test.cpp) +target_include_directories(step571_test PRIVATE src) +target_link_libraries(step571_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/TimeTravelDebugEventBuffer.h b/editor/src/TimeTravelDebugEventBuffer.h new file mode 100644 index 0000000..e487aec --- /dev/null +++ b/editor/src/TimeTravelDebugEventBuffer.h @@ -0,0 +1,86 @@ +#pragma once +// Step 571: Time-Travel Debug Event Buffer + +#include "DebugValidationUtil.h" + +#include +#include +#include +#include + +struct DebugReplayEvent { + std::string eventId; + std::string sessionId; + std::string type; + std::uint64_t instructionPointer = 0; + std::uint64_t timestamp = 0; + std::string payload; +}; + +class TimeTravelDebugEventBuffer { +public: + explicit TimeTravelDebugEventBuffer(std::size_t capacity = 128) + : capacity_(capacity == 0 ? 1 : capacity) {} + + bool append(const DebugReplayEvent& event, std::string* error) { + if (!error) return false; + error->clear(); + if (!hasRequiredDebugIds(event.eventId, event.sessionId)) return fail(error, "event_or_session_missing"); + if (event.type.empty()) return fail(error, "event_type_missing"); + if (event.timestamp == 0) return fail(error, "event_timestamp_invalid"); + if (event.instructionPointer == 0) return fail(error, "event_ip_invalid"); + if (containsId(event.eventId)) return fail(error, "event_duplicate"); + + events_.push_back(event); + while (events_.size() > capacity_) { + events_.pop_front(); + ++dropped_; + } + return true; + } + + std::size_t size() const { return events_.size(); } + std::size_t capacity() const { return capacity_; } + std::size_t droppedCount() const { return dropped_; } + + std::vector latest(std::size_t count) const { + std::vector out; + if (count == 0 || events_.empty()) return out; + const std::size_t begin = events_.size() > count ? events_.size() - count : 0; + for (std::size_t i = begin; i < events_.size(); ++i) out.push_back(events_[i]); + return out; + } + + std::vector forSession(const std::string& sessionId) const { + std::vector out; + for (const auto& event : events_) { + if (sessionId.empty() || event.sessionId == sessionId) out.push_back(event); + } + return out; + } + + std::vector replayWindow(std::size_t startIndex, std::size_t count) const { + std::vector out; + if (count == 0 || startIndex >= events_.size()) return out; + const std::size_t end = (startIndex + count) > events_.size() ? events_.size() : (startIndex + count); + for (std::size_t i = startIndex; i < end; ++i) out.push_back(events_[i]); + return out; + } + +private: + std::size_t capacity_ = 128; + std::size_t dropped_ = 0; + std::deque events_; + + static bool fail(std::string* error, const char* code) { + *error = code; + return false; + } + + bool containsId(const std::string& eventId) const { + for (const auto& event : events_) { + if (event.eventId == eventId) return true; + } + return false; + } +}; diff --git a/editor/tests/step571_test.cpp b/editor/tests/step571_test.cpp new file mode 100644 index 0000000..fa8bf6c --- /dev/null +++ b/editor/tests/step571_test.cpp @@ -0,0 +1,174 @@ +// Step 571: Time-Travel Debug Event Buffer (12 tests) + +#include "TimeTravelDebugEventBuffer.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 {} + +static DebugReplayEvent event(const std::string& id, + const std::string& session, + const std::string& type, + std::uint64_t ip, + std::uint64_t ts) { + DebugReplayEvent e; + e.eventId = id; + e.sessionId = session; + e.type = type; + e.instructionPointer = ip; + e.timestamp = ts; + e.payload = "payload-" + id; + return e; +} + +void test_append_success() { + TEST(append_success); + TimeTravelDebugEventBuffer buffer(4); + std::string error; + CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append should succeed"); + CHECK(buffer.size() == 1, "size mismatch"); + PASS(); +} + +void test_append_rejects_missing_ids() { + TEST(append_rejects_missing_ids); + TimeTravelDebugEventBuffer buffer(4); + std::string error; + CHECK(!buffer.append(event("", "sess-1", "step", 0x1000, 10), &error), "append should fail"); + CHECK(error == "event_or_session_missing", "wrong error"); + PASS(); +} + +void test_append_rejects_missing_type() { + TEST(append_rejects_missing_type); + TimeTravelDebugEventBuffer buffer(4); + std::string error; + CHECK(!buffer.append(event("e1", "sess-1", "", 0x1000, 10), &error), "append should fail"); + CHECK(error == "event_type_missing", "wrong error"); + PASS(); +} + +void test_append_rejects_invalid_timestamp() { + TEST(append_rejects_invalid_timestamp); + TimeTravelDebugEventBuffer buffer(4); + std::string error; + CHECK(!buffer.append(event("e1", "sess-1", "step", 0x1000, 0), &error), "append should fail"); + CHECK(error == "event_timestamp_invalid", "wrong error"); + PASS(); +} + +void test_append_rejects_invalid_ip() { + TEST(append_rejects_invalid_ip); + TimeTravelDebugEventBuffer buffer(4); + std::string error; + CHECK(!buffer.append(event("e1", "sess-1", "step", 0, 10), &error), "append should fail"); + CHECK(error == "event_ip_invalid", "wrong error"); + PASS(); +} + +void test_append_rejects_duplicate_event_id() { + TEST(append_rejects_duplicate_event_id); + TimeTravelDebugEventBuffer buffer(4); + std::string error; + CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "first append failed"); + CHECK(!buffer.append(event("e1", "sess-1", "step", 0x1001, 11), &error), "duplicate append should fail"); + CHECK(error == "event_duplicate", "wrong error"); + PASS(); +} + +void test_capacity_eviction_tracks_dropped_count() { + TEST(capacity_eviction_tracks_dropped_count); + TimeTravelDebugEventBuffer buffer(2); + std::string error; + CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append e1 failed"); + CHECK(buffer.append(event("e2", "sess-1", "step", 0x1001, 11), &error), "append e2 failed"); + CHECK(buffer.append(event("e3", "sess-1", "step", 0x1002, 12), &error), "append e3 failed"); + CHECK(buffer.size() == 2, "size should cap at capacity"); + CHECK(buffer.droppedCount() == 1, "dropped count mismatch"); + PASS(); +} + +void test_latest_returns_most_recent_events() { + TEST(latest_returns_most_recent_events); + TimeTravelDebugEventBuffer buffer(4); + std::string error; + CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append e1 failed"); + CHECK(buffer.append(event("e2", "sess-1", "step", 0x1001, 11), &error), "append e2 failed"); + CHECK(buffer.append(event("e3", "sess-1", "step", 0x1002, 12), &error), "append e3 failed"); + const auto latest = buffer.latest(2); + CHECK(latest.size() == 2, "latest count mismatch"); + CHECK(latest[0].eventId == "e2", "latest[0] mismatch"); + CHECK(latest[1].eventId == "e3", "latest[1] mismatch"); + PASS(); +} + +void test_for_session_filters_events() { + TEST(for_session_filters_events); + TimeTravelDebugEventBuffer buffer(4); + std::string error; + CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append e1 failed"); + CHECK(buffer.append(event("e2", "sess-2", "step", 0x1001, 11), &error), "append e2 failed"); + const auto filtered = buffer.forSession("sess-2"); + CHECK(filtered.size() == 1, "filter count mismatch"); + CHECK(filtered[0].eventId == "e2", "filter event mismatch"); + PASS(); +} + +void test_replay_window_returns_slice() { + TEST(replay_window_returns_slice); + TimeTravelDebugEventBuffer buffer(8); + std::string error; + CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append e1 failed"); + CHECK(buffer.append(event("e2", "sess-1", "step", 0x1001, 11), &error), "append e2 failed"); + CHECK(buffer.append(event("e3", "sess-1", "step", 0x1002, 12), &error), "append e3 failed"); + const auto window = buffer.replayWindow(1, 2); + CHECK(window.size() == 2, "replay window size mismatch"); + CHECK(window[0].eventId == "e2", "window[0] mismatch"); + CHECK(window[1].eventId == "e3", "window[1] mismatch"); + PASS(); +} + +void test_replay_window_out_of_range_is_empty() { + TEST(replay_window_out_of_range_is_empty); + TimeTravelDebugEventBuffer buffer(4); + std::string error; + CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append failed"); + const auto window = buffer.replayWindow(5, 2); + CHECK(window.empty(), "window should be empty"); + PASS(); +} + +void test_zero_capacity_defaults_to_one() { + TEST(zero_capacity_defaults_to_one); + TimeTravelDebugEventBuffer buffer(0); + std::string error; + CHECK(buffer.capacity() == 1, "capacity should default to one"); + CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append e1 failed"); + CHECK(buffer.append(event("e2", "sess-1", "step", 0x1001, 11), &error), "append e2 failed"); + CHECK(buffer.size() == 1, "size should remain one"); + PASS(); +} + +int main() { + std::cout << "Step 571: Time-Travel Debug Event Buffer\n"; + + test_append_success(); // 1 + test_append_rejects_missing_ids(); // 2 + test_append_rejects_missing_type(); // 3 + test_append_rejects_invalid_timestamp(); // 4 + test_append_rejects_invalid_ip(); // 5 + test_append_rejects_duplicate_event_id(); // 6 + test_capacity_eviction_tracks_dropped_count();// 7 + test_latest_returns_most_recent_events(); // 8 + test_for_session_filters_events(); // 9 + test_replay_window_returns_slice(); // 10 + test_replay_window_out_of_range_is_empty(); // 11 + test_zero_capacity_defaults_to_one(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 5597c0a..3a9444e 100644 --- a/progress.md +++ b/progress.md @@ -10405,3 +10405,38 @@ numeric/identifier evaluation. - `editor/src/WatchExpressionEvaluatorHardening.h` within header-size limit (`210` <= `600`) - `editor/tests/step570_test.cpp` within test-file size guidance (`142` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 571: Time-Travel Debug Event Buffer +**Status:** PASS (12/12 tests) + +Implements a bounded debug event history buffer for short-horizon time-travel +replay with deterministic eviction and session/window query helpers. + +**Files added:** +- `editor/src/TimeTravelDebugEventBuffer.h` - replay buffer module: + - bounded append lifecycle with overflow eviction tracking + - event validation (ids, type, timestamp, instruction pointer) + - latest-event and replay-window extraction helpers + - per-session event filtering + - duplicate event-id rejection behavior +- `editor/tests/step571_test.cpp` - 12 tests covering: + - append success and validation failures + - duplicate-id rejection behavior + - bounded eviction and dropped-count behavior + - latest/replay window query behavior + - per-session filtering behavior + - zero-capacity normalization behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step571_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step571_test step570_test` - PASS +- `./editor/build-native/step571_test` - PASS (12/12) +- `./editor/build-native/step570_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/TimeTravelDebugEventBuffer.h` within header-size limit (`86` <= `600`) +- `editor/tests/step571_test.cpp` within test-file size guidance (`174` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`