diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 6a08831..7a70eb9 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4459,4 +4459,13 @@ target_link_libraries(step619_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step620_test tests/step620_test.cpp) +target_include_directories(step620_test PRIVATE src) +target_link_libraries(step620_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/MCPProjectConfig.h b/editor/src/MCPProjectConfig.h index ca13b92..5a69de6 100644 --- a/editor/src/MCPProjectConfig.h +++ b/editor/src/MCPProjectConfig.h @@ -18,6 +18,7 @@ struct MCPProjectConfigData { struct MCPProjectConfigLoadResult { bool found = false; + std::string sourceWorkspaceRoot; MCPProjectConfigData config; std::string error; }; @@ -69,6 +70,7 @@ public: out.error = "workspace_missing"; return out; } + out.sourceWorkspaceRoot = workspaceRoot; std::filesystem::path path = std::filesystem::path(workspaceRoot) / ".whetstone.json"; if (!std::filesystem::exists(path)) return out; @@ -86,6 +88,30 @@ public: return out; } + static MCPProjectConfigLoadResult discoverFromCwd(const std::string& startDir = "") { + MCPProjectConfigLoadResult out; + std::filesystem::path current = startDir.empty() + ? std::filesystem::current_path() + : std::filesystem::path(startDir); + if (!std::filesystem::exists(current)) { + out.error = "start_dir_not_found"; + return out; + } + + current = std::filesystem::weakly_canonical(current); + while (true) { + std::filesystem::path candidate = current / ".whetstone.json"; + if (std::filesystem::exists(candidate)) { + return loadFromWorkspace(current.string()); + } + if (current == current.root_path()) break; + std::filesystem::path parent = current.parent_path(); + if (parent == current) break; + current = parent; + } + return out; + } + private: static bool readOptionalString(const json& parsed, const char* key, diff --git a/editor/src/mcp_main.cpp b/editor/src/mcp_main.cpp index d5d93ee..6dccba1 100644 --- a/editor/src/mcp_main.cpp +++ b/editor/src/mcp_main.cpp @@ -124,12 +124,19 @@ int main(int argc, char** argv) { if (!configLoad.error.empty()) { std::cerr << "[whetstone-mcp] Config load warning: " << configLoad.error << "\n"; } + } else { + configLoad = MCPProjectConfig::discoverFromCwd(); + if (!configLoad.error.empty()) { + std::cerr << "[whetstone-mcp] Config discovery warning: " << configLoad.error << "\n"; + } } if (configLoad.found && !configLoad.config.workspace.empty()) { state.workspaceRoot = configLoad.config.workspace; } else if (!workspace.empty()) { state.workspaceRoot = workspace; + } else if (configLoad.found) { + state.workspaceRoot = configLoad.sourceWorkspaceRoot; } if (!language.empty()) { diff --git a/editor/tests/step620_test.cpp b/editor/tests/step620_test.cpp new file mode 100644 index 0000000..d722e58 --- /dev/null +++ b/editor/tests/step620_test.cpp @@ -0,0 +1,191 @@ +// Step 620: Config auto-discovery (walk up from CWD) (12 tests) + +#include "MCPProjectConfig.h" + +#include +#include +#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 std::filesystem::path tempRoot() { + std::filesystem::path root = std::filesystem::temp_directory_path() / "whetstone_step620"; + std::filesystem::remove_all(root); + std::filesystem::create_directories(root); + return root; +} + +static void writeFile(const std::filesystem::path& path, const std::string& content) { + std::ofstream out(path); + out << content; +} + +void test_discover_from_exact_directory() { + TEST(discover_from_exact_directory); + auto root = tempRoot() / "exact"; + std::filesystem::create_directories(root); + writeFile(root / ".whetstone.json", R"({"defaultLanguage":"go"})"); + auto result = MCPProjectConfig::discoverFromCwd(root.string()); + CHECK(result.found, "config should be found"); + CHECK(result.sourceWorkspaceRoot == root.string(), "source workspace mismatch"); + CHECK(result.config.defaultLanguage == "go", "language mismatch"); + PASS(); +} + +void test_discover_walks_to_parent() { + TEST(discover_walks_to_parent); + auto root = tempRoot() / "parent"; + auto child = root / "src" / "feature"; + std::filesystem::create_directories(child); + writeFile(root / ".whetstone.json", R"({"defaultLanguage":"rust"})"); + auto result = MCPProjectConfig::discoverFromCwd(child.string()); + CHECK(result.found, "config should be found"); + CHECK(result.sourceWorkspaceRoot == root.string(), "should resolve parent workspace"); + PASS(); +} + +void test_discover_prefers_nearest_config() { + TEST(discover_prefers_nearest_config); + auto root = tempRoot() / "nearest"; + auto mid = root / "apps"; + auto leaf = mid / "api"; + std::filesystem::create_directories(leaf); + writeFile(root / ".whetstone.json", R"({"mcpWorkspaceAlias":"root"})"); + writeFile(mid / ".whetstone.json", R"({"mcpWorkspaceAlias":"mid"})"); + auto result = MCPProjectConfig::discoverFromCwd(leaf.string()); + CHECK(result.found, "config should be found"); + CHECK(result.sourceWorkspaceRoot == mid.string(), "should choose nearest parent"); + CHECK(result.config.mcpWorkspaceAlias == "mid", "alias should come from nearest file"); + PASS(); +} + +void test_discover_returns_not_found_when_missing() { + TEST(discover_returns_not_found_when_missing); + auto root = tempRoot() / "missing"; + auto child = root / "sub"; + std::filesystem::create_directories(child); + auto result = MCPProjectConfig::discoverFromCwd(child.string()); + CHECK(!result.found, "should not find config"); + CHECK(result.error.empty(), "error should be empty"); + PASS(); +} + +void test_discover_reports_invalid_start_directory() { + TEST(discover_reports_invalid_start_directory); + auto root = tempRoot() / "nope" / "does-not-exist"; + auto result = MCPProjectConfig::discoverFromCwd(root.string()); + CHECK(!result.found, "should not find config"); + CHECK(result.error == "start_dir_not_found", "wrong error"); + PASS(); +} + +void test_discover_reports_parse_error_from_nearest_file() { + TEST(discover_reports_parse_error_from_nearest_file); + auto root = tempRoot() / "bad"; + auto child = root / "src"; + std::filesystem::create_directories(child); + writeFile(root / ".whetstone.json", "{bad"); + auto result = MCPProjectConfig::discoverFromCwd(child.string()); + CHECK(result.found, "file exists so found should be true"); + CHECK(result.error == "config_json_parse_failed", "wrong error"); + PASS(); +} + +void test_discover_parses_workspace_override() { + TEST(discover_parses_workspace_override); + auto root = tempRoot() / "workspace-override"; + std::filesystem::create_directories(root); + writeFile(root / ".whetstone.json", R"({"workspace":"/opt/project"})"); + auto result = MCPProjectConfig::discoverFromCwd(root.string()); + CHECK(result.found, "should find config"); + CHECK(result.config.workspace == "/opt/project", "workspace override mismatch"); + PASS(); +} + +void test_discover_parses_agent_role() { + TEST(discover_parses_agent_role); + auto root = tempRoot() / "role"; + std::filesystem::create_directories(root); + writeFile(root / ".whetstone.json", R"({"agentRole":"generator"})"); + auto result = MCPProjectConfig::discoverFromCwd(root.string()); + CHECK(result.found, "should find config"); + CHECK(result.config.agentRole == "generator", "agent role mismatch"); + PASS(); +} + +void test_discover_parses_workspace_alias() { + TEST(discover_parses_workspace_alias); + auto root = tempRoot() / "alias"; + std::filesystem::create_directories(root); + writeFile(root / ".whetstone.json", R"({"mcpWorkspaceAlias":"payments"})"); + auto result = MCPProjectConfig::discoverFromCwd(root.string()); + CHECK(result.found, "should find config"); + CHECK(result.config.mcpWorkspaceAlias == "payments", "alias mismatch"); + PASS(); +} + +void test_discover_with_no_argument_uses_current_path() { + TEST(discover_with_no_argument_uses_current_path); + auto root = tempRoot() / "cwd-default"; + std::filesystem::create_directories(root); + writeFile(root / ".whetstone.json", R"({"defaultLanguage":"cpp"})"); + + auto previous = std::filesystem::current_path(); + std::filesystem::current_path(root); + auto result = MCPProjectConfig::discoverFromCwd(); + std::filesystem::current_path(previous); + + CHECK(result.found, "should find config from cwd"); + CHECK(result.config.defaultLanguage == "cpp", "language mismatch"); + PASS(); +} + +void test_discover_ignores_grandparent_when_parent_has_invalid_file() { + TEST(discover_ignores_grandparent_when_parent_has_invalid_file); + auto root = tempRoot() / "invalid-nearest"; + auto parent = root / "service"; + auto leaf = parent / "api"; + std::filesystem::create_directories(leaf); + writeFile(root / ".whetstone.json", R"({"defaultLanguage":"python"})"); + writeFile(parent / ".whetstone.json", "{bad"); + auto result = MCPProjectConfig::discoverFromCwd(leaf.string()); + CHECK(result.found, "nearest file exists"); + CHECK(result.error == "config_json_parse_failed", "should fail on nearest invalid file"); + PASS(); +} + +void test_discover_preserves_source_workspace_root_for_nested_start() { + TEST(discover_preserves_source_workspace_root_for_nested_start); + auto root = tempRoot() / "source-root"; + auto nested = root / "a" / "b" / "c"; + std::filesystem::create_directories(nested); + writeFile(root / ".whetstone.json", R"({"defaultLanguage":"typescript"})"); + auto result = MCPProjectConfig::discoverFromCwd(nested.string()); + CHECK(result.found, "should find config"); + CHECK(result.sourceWorkspaceRoot == root.string(), "source root mismatch"); + PASS(); +} + +int main() { + std::cout << "Step 620: Config auto-discovery walk-up\n"; + + test_discover_from_exact_directory(); // 1 + test_discover_walks_to_parent(); // 2 + test_discover_prefers_nearest_config(); // 3 + test_discover_returns_not_found_when_missing(); // 4 + test_discover_reports_invalid_start_directory(); // 5 + test_discover_reports_parse_error_from_nearest_file(); // 6 + test_discover_parses_workspace_override(); // 7 + test_discover_parses_agent_role(); // 8 + test_discover_parses_workspace_alias(); // 9 + test_discover_with_no_argument_uses_current_path(); // 10 + test_discover_ignores_grandparent_when_parent_has_invalid_file();// 11 + test_discover_preserves_source_workspace_root_for_nested_start();// 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 59a2b32..608a986 100644 --- a/progress.md +++ b/progress.md @@ -12252,3 +12252,38 @@ at startup when present. - `editor/src/mcp_main.cpp` within main-size limit (`177` <= `1500`) - `editor/tests/step619_test.cpp` within test-file size guidance (`169` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 620: Config auto-discovery (walk up from CWD) +**Status:** PASS (12/12 tests) + +Adds upward-walk config discovery so `whetstone_mcp` can boot without an +explicit `--workspace` flag by finding the nearest `.whetstone.json` from CWD. + +**Files modified:** +- `editor/src/MCPProjectConfig.h` - added: + - `discoverFromCwd()` walk-up discovery + - `sourceWorkspaceRoot` capture for discovered config location +- `editor/src/mcp_main.cpp` - startup now: + - uses discovery path when `--workspace` is omitted + - applies discovered workspace root when config file omits `workspace` + - emits discovery warnings on invalid start/config parse errors +- `editor/CMakeLists.txt` - `step620_test` target + +**Files added:** +- `editor/tests/step620_test.cpp` - 12 tests covering: + - exact/parent/nearest config discovery behavior + - no-config and invalid-start directory cases + - parse-error propagation from nearest config file + - field parsing during discovery and source-root tracking + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step620_test step619_test` - PASS +- `./editor/build-native/step620_test` - PASS (12/12) +- `./editor/build-native/step619_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/MCPProjectConfig.h` within header-size limit (`128` <= `600`) +- `editor/src/mcp_main.cpp` within main-size limit (`184` <= `1500`) +- `editor/tests/step620_test.cpp` within test-file size guidance (`191` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`