Add step 620 config auto-discovery from cwd
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
191
editor/tests/step620_test.cpp
Normal file
191
editor/tests/step620_test.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
// Step 620: Config auto-discovery (walk up from CWD) (12 tests)
|
||||
|
||||
#include "MCPProjectConfig.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
35
progress.md
35
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`
|
||||
|
||||
Reference in New Issue
Block a user