Step 421: add one-call workspace onboarding MCP tool
This commit is contained in:
@@ -2668,4 +2668,13 @@ target_link_libraries(step420_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step421_test tests/step421_test.cpp)
|
||||
target_include_directories(step421_test PRIVATE src)
|
||||
target_link_libraries(step421_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)
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
@@ -303,6 +305,189 @@ private:
|
||||
return resp;
|
||||
}
|
||||
|
||||
static bool hasCallError(const json& value) {
|
||||
return value.is_object() && value.contains("error");
|
||||
}
|
||||
|
||||
static void appendWarning(json& warnings, const std::string& message) {
|
||||
warnings.push_back(message);
|
||||
}
|
||||
|
||||
static std::string toLowerCopy(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
return s;
|
||||
}
|
||||
|
||||
static std::string languageForPath(const std::string& path) {
|
||||
auto dot = path.find_last_of('.');
|
||||
if (dot == std::string::npos) return "";
|
||||
std::string ext = toLowerCopy(path.substr(dot));
|
||||
if (ext == ".cpp" || ext == ".cc" || ext == ".cxx" ||
|
||||
ext == ".hpp" || ext == ".h" || ext == ".hh" ||
|
||||
ext == ".hxx") return "cpp";
|
||||
if (ext == ".py") return "python";
|
||||
if (ext == ".js" || ext == ".mjs" || ext == ".cjs") return "javascript";
|
||||
if (ext == ".ts" || ext == ".tsx") return "typescript";
|
||||
if (ext == ".java") return "java";
|
||||
if (ext == ".rs") return "rust";
|
||||
if (ext == ".go") return "go";
|
||||
return "";
|
||||
}
|
||||
|
||||
static bool isIgnoredOnboardPath(const std::string& path) {
|
||||
std::string lower = toLowerCopy(path);
|
||||
auto hasSegment = [&](const std::string& segment) {
|
||||
return lower.find(segment) == 0 ||
|
||||
lower.find("/" + segment) != std::string::npos;
|
||||
};
|
||||
return hasSegment(".git/") ||
|
||||
hasSegment("build/") ||
|
||||
hasSegment("build-native/") ||
|
||||
hasSegment("node_modules/") ||
|
||||
hasSegment(".whetstone/");
|
||||
}
|
||||
|
||||
static int filePriority(const std::string& path) {
|
||||
std::string lower = toLowerCopy(path);
|
||||
if (lower.find("main.") != std::string::npos ||
|
||||
lower.find("app.") != std::string::npos ||
|
||||
lower.find("index.") != std::string::npos ||
|
||||
lower.find("lib.") != std::string::npos) return 0;
|
||||
if (lower.find("/src/") != std::string::npos) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
static json selectOnboardingCandidates(const json& files, int maxFiles) {
|
||||
std::vector<std::pair<int, json>> ranked;
|
||||
std::map<std::string, int> languageCounts;
|
||||
json candidates = json::array();
|
||||
for (const auto& f : files) {
|
||||
if (!f.is_object()) continue;
|
||||
if (f.value("isDir", false)) continue;
|
||||
std::string path = f.value("path", "");
|
||||
if (path.empty() || isIgnoredOnboardPath(path)) continue;
|
||||
std::string language = languageForPath(path);
|
||||
if (language.empty()) continue;
|
||||
languageCounts[language] += 1;
|
||||
ranked.push_back({filePriority(path), {
|
||||
{"path", path},
|
||||
{"language", language}
|
||||
}});
|
||||
}
|
||||
std::sort(ranked.begin(), ranked.end(),
|
||||
[](const auto& a, const auto& b) {
|
||||
if (a.first != b.first) return a.first < b.first;
|
||||
return a.second.value("path", "") < b.second.value("path", "");
|
||||
});
|
||||
for (const auto& item : ranked) {
|
||||
if ((int)candidates.size() >= maxFiles) break;
|
||||
candidates.push_back(item.second);
|
||||
}
|
||||
json langJson = json::object();
|
||||
for (const auto& [lang, count] : languageCounts) langJson[lang] = count;
|
||||
return {
|
||||
{"candidates", candidates},
|
||||
{"languageCounts", langJson}
|
||||
};
|
||||
}
|
||||
|
||||
json runWorkspaceOnboarding(const json& args) {
|
||||
json warnings = json::array();
|
||||
int maxFiles = args.value("maxFiles", 8);
|
||||
if (maxFiles < 1) maxFiles = 1;
|
||||
if (maxFiles > 20) maxFiles = 20;
|
||||
|
||||
json indexParams = json::object();
|
||||
std::string root = args.value("root", "");
|
||||
if (!root.empty()) indexParams["root"] = root;
|
||||
json indexRes = callWhetstone("indexWorkspace", indexParams);
|
||||
if (hasCallError(indexRes)) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", indexRes["error"]},
|
||||
{"warnings", warnings}
|
||||
};
|
||||
}
|
||||
|
||||
json listRes = callWhetstone("workspaceList", json::object());
|
||||
json files = listRes.value("files", json::array());
|
||||
json selection = selectOnboardingCandidates(files, maxFiles);
|
||||
json candidates = selection["candidates"];
|
||||
json processed = json::array();
|
||||
int inferredCount = 0;
|
||||
int savedCount = 0;
|
||||
|
||||
for (const auto& c : candidates) {
|
||||
std::string path = c.value("path", "");
|
||||
std::string language = c.value("language", "");
|
||||
|
||||
json openRes = callWhetstone("openFile", {
|
||||
{"path", path},
|
||||
{"language", language}
|
||||
});
|
||||
if (hasCallError(openRes)) {
|
||||
appendWarning(warnings, "openFile failed for " + path);
|
||||
continue;
|
||||
}
|
||||
|
||||
json inferRes = callWhetstone("inferAnnotations", json::object());
|
||||
if (hasCallError(inferRes)) {
|
||||
appendWarning(warnings, "inferAnnotations failed for " + path);
|
||||
continue;
|
||||
}
|
||||
inferredCount += inferRes.value("count", 0);
|
||||
|
||||
std::string openedPath = openRes.value("path", path);
|
||||
json saveRes = callWhetstone("saveAnnotatedAST", {{"path", openedPath}});
|
||||
if (hasCallError(saveRes)) {
|
||||
appendWarning(warnings, "saveAnnotatedAST failed for " + openedPath);
|
||||
} else {
|
||||
savedCount += 1;
|
||||
}
|
||||
|
||||
processed.push_back({
|
||||
{"path", openedPath},
|
||||
{"language", language},
|
||||
{"suggestionCount", inferRes.value("count", 0)},
|
||||
{"saved", !hasCallError(saveRes)}
|
||||
});
|
||||
}
|
||||
|
||||
std::string readme =
|
||||
"# Whetstone Workspace\n"
|
||||
"This directory stores onboarding metadata and sidecar annotation artifacts.\n";
|
||||
json mkDirRes = callWhetstone("fileCreate", {
|
||||
{"path", ".whetstone/README.md"},
|
||||
{"content", readme}
|
||||
});
|
||||
if (hasCallError(mkDirRes))
|
||||
appendWarning(warnings, "Unable to create .whetstone/README.md");
|
||||
|
||||
return {
|
||||
{"success", true},
|
||||
{"workspace", indexRes.value("root", root)},
|
||||
{"fileCount", indexRes.value("fileCount", 0)},
|
||||
{"selectedFileCount", (int)candidates.size()},
|
||||
{"processedFileCount", (int)processed.size()},
|
||||
{"inferredAnnotationCount", inferredCount},
|
||||
{"savedSidecarCount", savedCount},
|
||||
{"languageCounts", selection["languageCounts"]},
|
||||
{"processedFiles", processed},
|
||||
{"workflowSuggestion", {
|
||||
{"nextTools", json::array({
|
||||
"whetstone_create_skeleton",
|
||||
"whetstone_create_workflow",
|
||||
"whetstone_orchestrate_run_deterministic",
|
||||
"whetstone_get_blockers",
|
||||
"whetstone_get_review_queue"
|
||||
})},
|
||||
{"note", "Onboarding complete. Use skeleton/workflow tools to start routed execution."}
|
||||
}},
|
||||
{"warnings", warnings}
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Step 208: Register AST query and mutation tools
|
||||
// ---------------------------------------------------------------
|
||||
@@ -1714,6 +1899,25 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
void registerOnboardingTools() {
|
||||
// whetstone_onboard_workspace
|
||||
tools_.push_back({"whetstone_onboard_workspace",
|
||||
"Run first-time workspace onboarding in one call: index files, detect key "
|
||||
"languages/files, run annotation inference on selected files, save sidecars, "
|
||||
"and bootstrap .whetstone metadata.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"root", {{"type", "string"},
|
||||
{"description", "Workspace root override (optional)"}}},
|
||||
{"maxFiles", {{"type", "integer"},
|
||||
{"description", "Max key files to process (default 8, max 20)"}}}
|
||||
}}}
|
||||
});
|
||||
toolHandlers_["whetstone_onboard_workspace"] =
|
||||
[this](const json& args) {
|
||||
return runWorkspaceOnboarding(args);
|
||||
};
|
||||
}
|
||||
|
||||
void registerWhetstoneTools() {
|
||||
registerASTTools();
|
||||
registerAnnotationTools();
|
||||
@@ -1731,5 +1935,6 @@ private:
|
||||
registerRoutingTools();
|
||||
registerOrchestratorTools();
|
||||
registerReviewTools();
|
||||
registerOnboardingTools();
|
||||
}
|
||||
};
|
||||
|
||||
355
editor/tests/step421_test.cpp
Normal file
355
editor/tests/step421_test.cpp
Normal file
@@ -0,0 +1,355 @@
|
||||
// Step 421: Workspace Onboarding Flow Tests (12 tests)
|
||||
|
||||
#include "MCPServer.h"
|
||||
#include "HeadlessEditorState.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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 json rpcResult(const json& result, int id = 1) {
|
||||
return {{"jsonrpc", "2.0"}, {"id", id}, {"result", result}};
|
||||
}
|
||||
|
||||
static json parseToolResult(const json& toolCallResponse) {
|
||||
if (!toolCallResponse.contains("result")) return json::object();
|
||||
auto content = toolCallResponse["result"].value("content", json::array());
|
||||
if (!content.is_array() || content.empty()) return json::object();
|
||||
std::string text = content[0].value("text", "{}");
|
||||
try {
|
||||
return json::parse(text);
|
||||
} catch (...) {
|
||||
return json::object();
|
||||
}
|
||||
}
|
||||
|
||||
static json callTool(MCPServer& server, const std::string& name,
|
||||
const json& args = json::object()) {
|
||||
auto resp = server.handleRequest({
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", 1},
|
||||
{"method", "tools/call"},
|
||||
{"params", {{"name", name}, {"arguments", args}}}
|
||||
});
|
||||
return parseToolResult(resp);
|
||||
}
|
||||
|
||||
static std::string makeTempDir(const std::string& name) {
|
||||
auto dir = std::filesystem::temp_directory_path() / ("whetstone_step421_" + name);
|
||||
std::filesystem::create_directories(dir);
|
||||
return dir.string();
|
||||
}
|
||||
|
||||
void test_onboard_tool_registered() {
|
||||
TEST(onboard_tool_registered);
|
||||
MCPServer server;
|
||||
bool found = false;
|
||||
for (const auto& t : server.getTools()) {
|
||||
if (t.name == "whetstone_onboard_workspace") found = true;
|
||||
}
|
||||
CHECK(found, "missing whetstone_onboard_workspace tool");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_calls_index_workspace_and_list() {
|
||||
TEST(onboard_calls_index_workspace_and_list);
|
||||
MCPServer server;
|
||||
std::vector<std::string> methods;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
std::string method = req.value("method", "");
|
||||
methods.push_back(method);
|
||||
if (method == "indexWorkspace")
|
||||
return rpcResult({{"root", "/repo"}, {"fileCount", 3}});
|
||||
if (method == "workspaceList")
|
||||
return rpcResult({{"files", json::array()}});
|
||||
if (method == "fileCreate")
|
||||
return rpcResult({{"created", ".whetstone/README.md"}});
|
||||
return rpcResult(json::object());
|
||||
});
|
||||
auto res = callTool(server, "whetstone_onboard_workspace");
|
||||
CHECK(res.value("success", false), "expected success");
|
||||
CHECK(methods.size() >= 2, "expected at least index+list calls");
|
||||
CHECK(methods[0] == "indexWorkspace", "first call should indexWorkspace");
|
||||
CHECK(methods[1] == "workspaceList", "second call should workspaceList");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_passes_root_to_index_workspace() {
|
||||
TEST(onboard_passes_root_to_index_workspace);
|
||||
MCPServer server;
|
||||
std::string rootParam;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
std::string method = req.value("method", "");
|
||||
if (method == "indexWorkspace") {
|
||||
rootParam = req["params"].value("root", "");
|
||||
return rpcResult({{"root", rootParam}, {"fileCount", 1}});
|
||||
}
|
||||
if (method == "workspaceList") return rpcResult({{"files", json::array()}});
|
||||
if (method == "fileCreate") return rpcResult({{"created", true}});
|
||||
return rpcResult(json::object());
|
||||
});
|
||||
auto res = callTool(server, "whetstone_onboard_workspace", {{"root", "/tmp/repo"}});
|
||||
CHECK(res.value("success", false), "expected success");
|
||||
CHECK(rootParam == "/tmp/repo", "root param not forwarded");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_respects_max_files_limit() {
|
||||
TEST(onboard_respects_max_files_limit);
|
||||
MCPServer server;
|
||||
int openCount = 0;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
std::string method = req.value("method", "");
|
||||
if (method == "indexWorkspace")
|
||||
return rpcResult({{"root", "/repo"}, {"fileCount", 4}});
|
||||
if (method == "workspaceList")
|
||||
return rpcResult({{"files", json::array({
|
||||
{{"path", "a.cpp"}, {"isDir", false}},
|
||||
{{"path", "b.cpp"}, {"isDir", false}},
|
||||
{{"path", "c.py"}, {"isDir", false}},
|
||||
{{"path", "d.ts"}, {"isDir", false}}
|
||||
})}});
|
||||
if (method == "openFile") { ++openCount; return rpcResult(req["params"]); }
|
||||
if (method == "inferAnnotations") return rpcResult({{"count", 2}});
|
||||
if (method == "saveAnnotatedAST") return rpcResult({{"success", true}});
|
||||
if (method == "fileCreate") return rpcResult({{"created", true}});
|
||||
return rpcResult(json::object());
|
||||
});
|
||||
auto res = callTool(server, "whetstone_onboard_workspace", {{"maxFiles", 2}});
|
||||
CHECK(res.value("success", false), "expected success");
|
||||
CHECK(openCount == 2, "expected openFile called exactly maxFiles times");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_filters_ignored_and_non_source_files() {
|
||||
TEST(onboard_filters_ignored_and_non_source_files);
|
||||
MCPServer server;
|
||||
int openCount = 0;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
std::string method = req.value("method", "");
|
||||
if (method == "indexWorkspace")
|
||||
return rpcResult({{"root", "/repo"}, {"fileCount", 6}});
|
||||
if (method == "workspaceList")
|
||||
return rpcResult({{"files", json::array({
|
||||
{{"path", "src/main.cpp"}, {"isDir", false}},
|
||||
{{"path", "README.md"}, {"isDir", false}},
|
||||
{{"path", "build/tmp.cpp"}, {"isDir", false}},
|
||||
{{"path", "node_modules/pkg/index.js"}, {"isDir", false}},
|
||||
{{"path", ".whetstone/cache.py"}, {"isDir", false}},
|
||||
{{"path", "scripts/run.sh"}, {"isDir", false}}
|
||||
})}});
|
||||
if (method == "openFile") { ++openCount; return rpcResult(req["params"]); }
|
||||
if (method == "inferAnnotations") return rpcResult({{"count", 1}});
|
||||
if (method == "saveAnnotatedAST") return rpcResult({{"success", true}});
|
||||
if (method == "fileCreate") return rpcResult({{"created", true}});
|
||||
return rpcResult(json::object());
|
||||
});
|
||||
auto res = callTool(server, "whetstone_onboard_workspace");
|
||||
CHECK(res.value("success", false), "expected success");
|
||||
CHECK(openCount == 1, "expected only source file outside ignored dirs");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_reports_language_counts() {
|
||||
TEST(onboard_reports_language_counts);
|
||||
MCPServer server;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
std::string method = req.value("method", "");
|
||||
if (method == "indexWorkspace")
|
||||
return rpcResult({{"root", "/repo"}, {"fileCount", 4}});
|
||||
if (method == "workspaceList")
|
||||
return rpcResult({{"files", json::array({
|
||||
{{"path", "a.cpp"}, {"isDir", false}},
|
||||
{{"path", "b.cpp"}, {"isDir", false}},
|
||||
{{"path", "c.py"}, {"isDir", false}},
|
||||
{{"path", "d.go"}, {"isDir", false}}
|
||||
})}});
|
||||
if (method == "openFile") return rpcResult(req["params"]);
|
||||
if (method == "inferAnnotations") return rpcResult({{"count", 1}});
|
||||
if (method == "saveAnnotatedAST") return rpcResult({{"success", true}});
|
||||
if (method == "fileCreate") return rpcResult({{"created", true}});
|
||||
return rpcResult(json::object());
|
||||
});
|
||||
auto res = callTool(server, "whetstone_onboard_workspace");
|
||||
CHECK(res["languageCounts"].value("cpp", 0) == 2, "cpp count mismatch");
|
||||
CHECK(res["languageCounts"].value("python", 0) == 1, "python count mismatch");
|
||||
CHECK(res["languageCounts"].value("go", 0) == 1, "go count mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_aggregates_inference_and_sidecar_counts() {
|
||||
TEST(onboard_aggregates_inference_and_sidecar_counts);
|
||||
MCPServer server;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
std::string method = req.value("method", "");
|
||||
if (method == "indexWorkspace")
|
||||
return rpcResult({{"root", "/repo"}, {"fileCount", 2}});
|
||||
if (method == "workspaceList")
|
||||
return rpcResult({{"files", json::array({
|
||||
{{"path", "a.cpp"}, {"isDir", false}},
|
||||
{{"path", "b.py"}, {"isDir", false}}
|
||||
})}});
|
||||
if (method == "openFile") return rpcResult(req["params"]);
|
||||
if (method == "inferAnnotations") return rpcResult({{"count", 3}});
|
||||
if (method == "saveAnnotatedAST") return rpcResult({{"success", true}});
|
||||
if (method == "fileCreate") return rpcResult({{"created", true}});
|
||||
return rpcResult(json::object());
|
||||
});
|
||||
auto res = callTool(server, "whetstone_onboard_workspace");
|
||||
CHECK(res.value("inferredAnnotationCount", -1) == 6, "inference count mismatch");
|
||||
CHECK(res.value("savedSidecarCount", -1) == 2, "saved sidecar count mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_warns_and_continues_on_open_failure() {
|
||||
TEST(onboard_warns_and_continues_on_open_failure);
|
||||
MCPServer server;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
std::string method = req.value("method", "");
|
||||
if (method == "indexWorkspace") return rpcResult({{"root", "/repo"}, {"fileCount", 2}});
|
||||
if (method == "workspaceList") return rpcResult({{"files", json::array({
|
||||
{{"path", "a.cpp"}, {"isDir", false}},
|
||||
{{"path", "b.py"}, {"isDir", false}}
|
||||
})}});
|
||||
if (method == "openFile") {
|
||||
std::string path = req["params"].value("path", "");
|
||||
if (path == "a.cpp")
|
||||
return json{{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"error", {{"code", -1}, {"message", "open fail"}}}};
|
||||
return rpcResult(req["params"]);
|
||||
}
|
||||
if (method == "inferAnnotations") return rpcResult({{"count", 2}});
|
||||
if (method == "saveAnnotatedAST") return rpcResult({{"success", true}});
|
||||
if (method == "fileCreate") return rpcResult({{"created", true}});
|
||||
return rpcResult(json::object());
|
||||
});
|
||||
auto res = callTool(server, "whetstone_onboard_workspace");
|
||||
CHECK(res.value("success", false), "expected overall success");
|
||||
CHECK(res.value("processedFileCount", -1) == 1, "expected one processed file");
|
||||
CHECK(res["warnings"].is_array() && !res["warnings"].empty(), "expected warnings");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_warns_and_continues_on_infer_failure() {
|
||||
TEST(onboard_warns_and_continues_on_infer_failure);
|
||||
MCPServer server;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
std::string method = req.value("method", "");
|
||||
if (method == "indexWorkspace") return rpcResult({{"root", "/repo"}, {"fileCount", 2}});
|
||||
if (method == "workspaceList") return rpcResult({{"files", json::array({
|
||||
{{"path", "a.cpp"}, {"isDir", false}},
|
||||
{{"path", "b.py"}, {"isDir", false}}
|
||||
})}});
|
||||
if (method == "openFile") return rpcResult(req["params"]);
|
||||
if (method == "inferAnnotations") {
|
||||
static int callIdx = 0;
|
||||
++callIdx;
|
||||
if (callIdx == 1)
|
||||
return json{{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"error", {{"code", -1}, {"message", "infer fail"}}}};
|
||||
return rpcResult({{"count", 4}});
|
||||
}
|
||||
if (method == "saveAnnotatedAST") return rpcResult({{"success", true}});
|
||||
if (method == "fileCreate") return rpcResult({{"created", true}});
|
||||
return rpcResult(json::object());
|
||||
});
|
||||
auto res = callTool(server, "whetstone_onboard_workspace");
|
||||
CHECK(res.value("success", false), "expected overall success");
|
||||
CHECK(res.value("processedFileCount", -1) == 1, "expected one processed file");
|
||||
CHECK(res.value("savedSidecarCount", -1) == 1, "expected one saved sidecar");
|
||||
CHECK(res["warnings"].is_array() && !res["warnings"].empty(), "expected warnings");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_returns_failure_when_index_fails() {
|
||||
TEST(onboard_returns_failure_when_index_fails);
|
||||
MCPServer server;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
if (req.value("method", "") == "indexWorkspace")
|
||||
return json{{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"error", {{"code", -32602}, {"message", "missing root"}}}};
|
||||
return rpcResult(json::object());
|
||||
});
|
||||
auto res = callTool(server, "whetstone_onboard_workspace");
|
||||
CHECK(!res.value("success", true), "expected failure");
|
||||
CHECK(res.contains("error"), "expected error payload");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_creates_whetstone_readme_and_suggestion() {
|
||||
TEST(onboard_creates_whetstone_readme_and_suggestion);
|
||||
MCPServer server;
|
||||
std::string createdPath;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
std::string method = req.value("method", "");
|
||||
if (method == "indexWorkspace") return rpcResult({{"root", "/repo"}, {"fileCount", 1}});
|
||||
if (method == "workspaceList") return rpcResult({{"files", json::array({{{"path", "a.cpp"}, {"isDir", false}}})}});
|
||||
if (method == "openFile") return rpcResult(req["params"]);
|
||||
if (method == "inferAnnotations") return rpcResult({{"count", 1}});
|
||||
if (method == "saveAnnotatedAST") return rpcResult({{"success", true}});
|
||||
if (method == "fileCreate") {
|
||||
createdPath = req["params"].value("path", "");
|
||||
return rpcResult({{"created", true}});
|
||||
}
|
||||
return rpcResult(json::object());
|
||||
});
|
||||
auto res = callTool(server, "whetstone_onboard_workspace");
|
||||
CHECK(createdPath == ".whetstone/README.md", "expected readme bootstrap path");
|
||||
CHECK(res["workflowSuggestion"]["nextTools"].is_array(), "missing nextTools suggestion");
|
||||
CHECK(!res["workflowSuggestion"].value("note", "").empty(), "missing suggestion note");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_onboard_headless_end_to_end() {
|
||||
TEST(onboard_headless_end_to_end);
|
||||
auto root = std::filesystem::path(makeTempDir("e2e"));
|
||||
std::filesystem::create_directories(root / "src");
|
||||
std::ofstream(root / "src" / "main.cpp") << "int add(int a,int b){return a+b;}\n";
|
||||
std::ofstream(root / "src" / "util.py") << "def inc(x):\n return x + 1\n";
|
||||
|
||||
HeadlessEditorState state;
|
||||
state.setAgentRole("sess", AgentRole::Refactor);
|
||||
state.workspaceRoot = root.string();
|
||||
|
||||
MCPServer server;
|
||||
server.setRpcCallback([&](const json& req) {
|
||||
return state.processAgentRequest(req, "sess");
|
||||
});
|
||||
|
||||
auto res = callTool(server, "whetstone_onboard_workspace",
|
||||
{{"root", root.string()}, {"maxFiles", 2}});
|
||||
CHECK(res.value("success", false), "expected onboarding success");
|
||||
CHECK(res.value("processedFileCount", 0) >= 1, "expected at least one processed file");
|
||||
CHECK(std::filesystem::exists(root / ".whetstone" / "README.md"),
|
||||
"expected .whetstone readme");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 421: Workspace Onboarding Flow Tests\n";
|
||||
|
||||
test_onboard_tool_registered(); // 1
|
||||
test_onboard_calls_index_workspace_and_list(); // 2
|
||||
test_onboard_passes_root_to_index_workspace(); // 3
|
||||
test_onboard_respects_max_files_limit(); // 4
|
||||
test_onboard_filters_ignored_and_non_source_files(); // 5
|
||||
test_onboard_reports_language_counts(); // 6
|
||||
test_onboard_aggregates_inference_and_sidecar_counts(); // 7
|
||||
test_onboard_warns_and_continues_on_open_failure(); // 8
|
||||
test_onboard_warns_and_continues_on_infer_failure(); // 9
|
||||
test_onboard_returns_failure_when_index_fails(); // 10
|
||||
test_onboard_creates_whetstone_readme_and_suggestion(); // 11
|
||||
test_onboard_headless_end_to_end(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed)
|
||||
<< " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
46
progress.md
46
progress.md
@@ -4424,6 +4424,52 @@ headless JSON-RPC path.
|
||||
- `editor/src/MCPServer.h` (`1735` > `600`)
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` (`2768` > `600`)
|
||||
|
||||
### Step 421: Workspace Onboarding Flow
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Added one-call MCP workspace onboarding so a client can bootstrap workflow
|
||||
state on a new repo by indexing files, selecting key source files, running
|
||||
annotation inference, saving sidecars, and creating `.whetstone` metadata.
|
||||
|
||||
**Files created:**
|
||||
- `editor/tests/step421_test.cpp` — 12 tests covering:
|
||||
1. `whetstone_onboard_workspace` tool registration
|
||||
2. index/list call sequencing
|
||||
3. `root` forwarding to `indexWorkspace`
|
||||
4. `maxFiles` processing cap
|
||||
5. ignored/non-source path filtering
|
||||
6. language-count aggregation
|
||||
7. inference + sidecar aggregate counters
|
||||
8. open-file failure warning/continue behavior
|
||||
9. infer failure warning/continue behavior
|
||||
10. index failure hard-stop behavior
|
||||
11. `.whetstone/README.md` bootstrap + workflow suggestion payload
|
||||
12. headless end-to-end onboarding against a temp workspace
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/MCPServer.h` — added Step 421 onboarding support:
|
||||
- `whetstone_onboard_workspace` tool registration
|
||||
- composite onboarding execution (`runWorkspaceOnboarding`) with:
|
||||
- `indexWorkspace`
|
||||
- `workspaceList`
|
||||
- candidate source-file selection + language detection
|
||||
- `openFile` + `inferAnnotations` + `saveAnnotatedAST` loop
|
||||
- `.whetstone/README.md` bootstrap via `fileCreate`
|
||||
- warning collection and next-tool workflow suggestion
|
||||
- `editor/CMakeLists.txt` — `step421_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `step421_test` — PASS (12/12) new step coverage
|
||||
- `step420_test` — PASS (12/12) regression coverage
|
||||
- `step419_test` — PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/tests/step421_test.cpp` within test-file size guidance (`355` lines)
|
||||
- Legacy oversized headers persist:
|
||||
- `editor/src/ast/Serialization.h` (`1427` > `600`)
|
||||
- `editor/src/MCPServer.h` (`1940` > `600`)
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` (`2768` > `600`)
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)
|
||||
|
||||
Reference in New Issue
Block a user