From e215ee871108873d7b92500dfbc63db975bb442e Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 22:47:55 -0700 Subject: [PATCH] Step 506: add edge-case cleanup guards and tests --- editor/CMakeLists.txt | 9 ++ editor/src/EdgeCaseCleanup.h | 139 +++++++++++++++++++++++++++++++ editor/tests/step506_test.cpp | 150 ++++++++++++++++++++++++++++++++++ progress.md | 34 ++++++++ 4 files changed, 332 insertions(+) create mode 100644 editor/src/EdgeCaseCleanup.h create mode 100644 editor/tests/step506_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index ad6192d..11f059f 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3433,4 +3433,13 @@ target_link_libraries(step505_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step506_test tests/step506_test.cpp) +target_include_directories(step506_test PRIVATE src) +target_link_libraries(step506_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/EdgeCaseCleanup.h b/editor/src/EdgeCaseCleanup.h new file mode 100644 index 0000000..0d9eeb1 --- /dev/null +++ b/editor/src/EdgeCaseCleanup.h @@ -0,0 +1,139 @@ +#pragma once + +// Step 506: Edge Case Cleanup +// Guard rails for common failure modes with explicit error semantics. + +#include +#include +#include +#include +#include +#include + +struct EdgeCaseResult { + bool ok = true; + std::string code; // e.g. E_EMPTY_FILE, E_BINARY_FILE, ... + std::string message; +}; + +class EdgeCaseCleanup { +public: + static EdgeCaseResult inspectFileContent(const std::string& content, + std::size_t maxBytes = 1024 * 1024) { + if (content.empty()) return fail("E_EMPTY_FILE", "File is empty"); + if (isBinary(content)) return fail("E_BINARY_FILE", "Binary file detected"); + if (content.size() > maxBytes) return fail("E_FILE_TOO_LARGE", "File exceeds size threshold"); + return ok("File content accepted"); + } + + static EdgeCaseResult validateAnnotations(const std::vector& annotations) { + for (const auto& a : annotations) { + if (a.empty() || a[0] != '@') { + return fail("E_MALFORMED_ANNOTATION", "Annotation must begin with '@'"); + } + if (a.find('(') != std::string::npos && a.find(')') == std::string::npos) { + return fail("E_MALFORMED_ANNOTATION", "Unbalanced annotation parentheses"); + } + } + return ok("Annotations valid"); + } + + static EdgeCaseResult detectCircularDependencies( + const std::map>& graph) { + std::set visited, stack; + for (const auto& kv : graph) { + if (dfsCycle(kv.first, graph, visited, stack)) + return fail("E_CIRCULAR_DEPENDENCY", "Circular dependency detected"); + } + return ok("No circular dependencies"); + } + + static EdgeCaseResult checkConcurrentModification(int expectedVersion, int currentVersion) { + if (expectedVersion != currentVersion) { + return fail("E_CONCURRENT_MODIFICATION", + "Workflow state changed concurrently; reload required"); + } + return ok("No concurrent modification conflict"); + } + + static EdgeCaseResult saveInterruptedWorkflow(const std::string& path, + const std::string& workflowJson) { + namespace fs = std::filesystem; + fs::path p(path); + std::error_code ec; + fs::create_directories(p.parent_path(), ec); + if (ec) return fail("E_WORKFLOW_SAVE_FAILED", "Unable to create workflow directory"); + + std::ofstream out(path); + if (!out) return fail("E_WORKFLOW_SAVE_FAILED", "Unable to open workflow file for write"); + out << workflowJson; + return ok("Workflow saved"); + } + + static EdgeCaseResult resumeInterruptedWorkflow(const std::string& path, + std::string* workflowJson = nullptr) { + std::ifstream in(path); + if (!in) return fail("E_WORKFLOW_RESUME_FAILED", "Workflow state file missing"); + std::string content((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + if (content.empty()) return fail("E_WORKFLOW_RESUME_FAILED", "Workflow state file is empty"); + if (workflowJson) *workflowJson = content; + return ok("Workflow resumed"); + } + + static std::string normalizeLanguage(const std::string& language) { + if (language == "python" || language == "cpp" || language == "c" || + language == "rust" || language == "go" || language == "java" || + language == "javascript" || language == "typescript") { + return language; + } + return "plain_text"; + } + + static EdgeCaseResult validateMcpRequest(const std::string& requestJson) { + if (requestJson.empty()) { + return fail("E_MCP_MALFORMED_REQUEST", "Request payload is empty"); + } + if (requestJson.front() != '{' || requestJson.back() != '}') { + return fail("E_MCP_MALFORMED_REQUEST", "Request is not valid JSON object text"); + } + if (requestJson.find("\"method\"") == std::string::npos) { + return fail("E_MCP_MALFORMED_REQUEST", "Missing required field: method"); + } + return ok("MCP request valid"); + } + +private: + static EdgeCaseResult ok(const std::string& message) { + return {true, "", message}; + } + + static EdgeCaseResult fail(const std::string& code, const std::string& message) { + return {false, code, message}; + } + + static bool isBinary(const std::string& content) { + for (unsigned char c : content) { + if (c == 0) return true; + } + return false; + } + + static bool dfsCycle(const std::string& node, + const std::map>& graph, + std::set& visited, + std::set& stack) { + if (stack.count(node)) return true; + if (visited.count(node)) return false; + visited.insert(node); + stack.insert(node); + + auto it = graph.find(node); + if (it != graph.end()) { + for (const auto& next : it->second) { + if (dfsCycle(next, graph, visited, stack)) return true; + } + } + stack.erase(node); + return false; + } +}; diff --git a/editor/tests/step506_test.cpp b/editor/tests/step506_test.cpp new file mode 100644 index 0000000..fed5c95 --- /dev/null +++ b/editor/tests/step506_test.cpp @@ -0,0 +1,150 @@ +// Step 506: Edge Case Cleanup Tests (12 tests) + +#include "EdgeCaseCleanup.h" + +#include +#include + +namespace fs = std::filesystem; + +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_empty_file_returns_meaningful_error() { + TEST(empty_file_returns_meaningful_error); + auto r = EdgeCaseCleanup::inspectFileContent(""); + CHECK(!r.ok, "empty file should fail"); + CHECK(r.code == "E_EMPTY_FILE", "unexpected empty-file code"); + PASS(); +} + +void test_binary_file_returns_meaningful_error() { + TEST(binary_file_returns_meaningful_error); + std::string s = "abc"; + s.push_back('\0'); + s += "def"; + auto r = EdgeCaseCleanup::inspectFileContent(s); + CHECK(!r.ok, "binary file should fail"); + CHECK(r.code == "E_BINARY_FILE", "unexpected binary-file code"); + PASS(); +} + +void test_large_file_returns_meaningful_error() { + TEST(large_file_returns_meaningful_error); + std::string s(2048, 'x'); + auto r = EdgeCaseCleanup::inspectFileContent(s, 1024); + CHECK(!r.ok, "large file should fail"); + CHECK(r.code == "E_FILE_TOO_LARGE", "unexpected large-file code"); + PASS(); +} + +void test_malformed_annotations_return_meaningful_error() { + TEST(malformed_annotations_return_meaningful_error); + auto r = EdgeCaseCleanup::validateAnnotations({"Intent(no_prefix)"}); + CHECK(!r.ok, "malformed annotation should fail"); + CHECK(r.code == "E_MALFORMED_ANNOTATION", "unexpected annotation error code"); + PASS(); +} + +void test_circular_dependencies_return_meaningful_error() { + TEST(circular_dependencies_return_meaningful_error); + std::map> g = { + {"a", {"b"}}, {"b", {"c"}}, {"c", {"a"}} + }; + auto r = EdgeCaseCleanup::detectCircularDependencies(g); + CHECK(!r.ok, "circular dependency should fail"); + CHECK(r.code == "E_CIRCULAR_DEPENDENCY", "unexpected circular dependency code"); + PASS(); +} + +void test_concurrent_workflow_modifications_return_conflict_error() { + TEST(concurrent_workflow_modifications_return_conflict_error); + auto r = EdgeCaseCleanup::checkConcurrentModification(5, 6); + CHECK(!r.ok, "concurrent modification should fail"); + CHECK(r.code == "E_CONCURRENT_MODIFICATION", "unexpected conflict error code"); + PASS(); +} + +void test_interrupted_workflow_can_save_and_resume() { + TEST(interrupted_workflow_can_save_and_resume); + fs::path p = fs::temp_directory_path() / "whetstone_step506" / "wf.json"; + fs::remove_all(p.parent_path()); + + auto save = EdgeCaseCleanup::saveInterruptedWorkflow(p.string(), "{\"state\":\"paused\"}"); + CHECK(save.ok, "save should succeed"); + + std::string resumed; + auto resume = EdgeCaseCleanup::resumeInterruptedWorkflow(p.string(), &resumed); + CHECK(resume.ok, "resume should succeed"); + CHECK(resumed.find("paused") != std::string::npos, "resumed state mismatch"); + + fs::remove_all(p.parent_path()); + PASS(); +} + +void test_resume_missing_workflow_returns_meaningful_error() { + TEST(resume_missing_workflow_returns_meaningful_error); + fs::path p = fs::temp_directory_path() / "whetstone_step506_missing" / "wf.json"; + fs::remove_all(p.parent_path()); + auto r = EdgeCaseCleanup::resumeInterruptedWorkflow(p.string(), nullptr); + CHECK(!r.ok, "resume should fail for missing file"); + CHECK(r.code == "E_WORKFLOW_RESUME_FAILED", "unexpected resume error code"); + PASS(); +} + +void test_unknown_language_falls_back_to_plain_text() { + TEST(unknown_language_falls_back_to_plain_text); + auto lang = EdgeCaseCleanup::normalizeLanguage("kotlin"); + CHECK(lang == "plain_text", "unknown language should fallback to plain_text"); + PASS(); +} + +void test_mcp_malformed_request_returns_meaningful_error() { + TEST(mcp_malformed_request_returns_meaningful_error); + auto r = EdgeCaseCleanup::validateMcpRequest("[]"); + CHECK(!r.ok, "malformed request should fail"); + CHECK(r.code == "E_MCP_MALFORMED_REQUEST", "unexpected MCP error code"); + PASS(); +} + +void test_valid_input_path_returns_ok_without_crash() { + TEST(valid_input_path_returns_ok_without_crash); + auto fileCheck = EdgeCaseCleanup::inspectFileContent("int main(){}"); + auto annCheck = EdgeCaseCleanup::validateAnnotations({"@Intent(parse)"}); + auto reqCheck = EdgeCaseCleanup::validateMcpRequest("{\"method\":\"ping\"}"); + CHECK(fileCheck.ok && annCheck.ok && reqCheck.ok, "valid path should succeed"); + PASS(); +} + +void test_all_error_paths_include_nonempty_messages() { + TEST(all_error_paths_include_nonempty_messages); + auto a = EdgeCaseCleanup::inspectFileContent(""); + auto b = EdgeCaseCleanup::validateMcpRequest(""); + CHECK(!a.message.empty(), "empty-file message missing"); + CHECK(!b.message.empty(), "mcp-error message missing"); + PASS(); +} + +int main() { + std::cout << "Step 506: Edge Case Cleanup Tests\n"; + + test_empty_file_returns_meaningful_error(); // 1 + test_binary_file_returns_meaningful_error(); // 2 + test_large_file_returns_meaningful_error(); // 3 + test_malformed_annotations_return_meaningful_error(); // 4 + test_circular_dependencies_return_meaningful_error(); // 5 + test_concurrent_workflow_modifications_return_conflict_error(); // 6 + test_interrupted_workflow_can_save_and_resume(); // 7 + test_resume_missing_workflow_returns_meaningful_error(); // 8 + test_unknown_language_falls_back_to_plain_text(); // 9 + test_mcp_malformed_request_returns_meaningful_error(); // 10 + test_valid_input_path_returns_ok_without_crash(); // 11 + test_all_error_paths_include_nonempty_messages(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 1e3842f..1bbca7f 100644 --- a/progress.md +++ b/progress.md @@ -7856,3 +7856,37 @@ resources for MCP client consumption. - `editor/src/MCPToolDocumentation.h` within header-size limit (`192` <= `600`) - `editor/tests/step505_test.cpp` within test-file size guidance (`167` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 506: Edge Case Cleanup +**Status:** PASS (12/12 tests) + +Implements explicit guardrails for high-risk edge cases with stable error codes +and non-crashing fallback behavior across file handling, workflow persistence, +dependency validation, language fallback, and MCP request validation. + +**Files added:** +- `editor/src/EdgeCaseCleanup.h` - edge-case guard module: + - empty/binary/oversized file detection + - malformed annotation validation + - circular dependency detection + - concurrent workflow modification conflict detection + - interrupted workflow save/resume helpers + - unknown-language fallback to `plain_text` + - malformed MCP request validation +- `editor/tests/step506_test.cpp` - 12 tests covering: + - each edge-case error path + code/message semantics + - workflow save/resume success/failure behavior + - language fallback behavior + - valid-path no-crash behavior +- `editor/CMakeLists.txt` - `step506_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step506_test step505_test` - PASS +- `./editor/build-native/step506_test` - PASS (12/12) +- `./editor/build-native/step505_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/EdgeCaseCleanup.h` within header-size limit (`139` <= `600`) +- `editor/tests/step506_test.cpp` within test-file size guidance (`150` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`