Files
whetstone_DSL/editor/tests/step615_test.cpp

398 lines
16 KiB
C++

// Step 615: whetstone_generate_taskitems MCP Tool (13 tests)
#include "MCPServer.h"
#include <iostream>
#include <string>
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 callTool(MCPServer& mcp, const std::string& name, const json& args) {
json req = {
{"jsonrpc", "2.0"},
{"id", 1},
{"method", "tools/call"},
{"params", {{"name", name}, {"arguments", args}}}
};
json resp = mcp.handleRequest(req);
std::string text = resp["result"]["content"][0].value("text", "{}");
return json::parse(text);
}
static std::string clearSpec() {
return
"## Goals\n"
"- Ship intake parser\n"
"\n"
"## Constraints\n"
"- Keep modules header only\n"
"\n"
"## Dependencies\n"
"- nlohmann json\n"
"\n"
"## Acceptance Criteria\n"
"- tests pass\n";
}
static std::string riskySpec() {
return
"## Goals\n"
"- maybe improve intake and maybe improve review\n"
"\n"
"## Constraints\n"
"- Do not use dynamic allocation in hot path\n"
"- Use dynamic allocation in hot path for warmup\n"
"\n"
"## Dependencies\n"
"- maybe add cache lib\n"
"\n"
"## Acceptance Criteria\n"
"- maybe latency under 50ms\n";
}
static json intakeOutput(MCPServer& mcp, const std::string& markdown) {
return callTool(mcp, "whetstone_architect_intake", {{"markdown", markdown}});
}
static json taskitemArgsFromIntake(const json& intake) {
return {
{"normalizedRequirements", intake["normalizedRequirements"]},
{"conflicts", intake["conflicts"]}
};
}
void test_tool_registered_accessor() {
TEST(tool_registered_accessor);
MCPServer mcp;
bool found = false;
for (const auto& tool : mcp.getTools()) {
if (tool.name == "whetstone_generate_taskitems") found = true;
}
CHECK(found, "tool should be registered");
PASS();
}
void test_tool_schema_requires_normalized_requirements() {
TEST(tool_schema_requires_normalized_requirements);
MCPServer mcp;
bool required = false;
for (const auto& tool : mcp.getTools()) {
if (tool.name != "whetstone_generate_taskitems") continue;
for (const auto& req : tool.inputSchema["required"]) {
if (req == "normalizedRequirements") required = true;
}
}
CHECK(required, "normalizedRequirements should be required");
PASS();
}
void test_success_for_intake_output() {
TEST(success_for_intake_output);
MCPServer mcp;
json intake = intakeOutput(mcp, clearSpec());
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
CHECK(out.value("success", false), "success should be true");
PASS();
}
void test_returns_non_empty_tasks() {
TEST(returns_non_empty_tasks);
MCPServer mcp;
json intake = intakeOutput(mcp, clearSpec());
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
CHECK(out["tasks"].is_array() && !out["tasks"].empty(), "tasks should be non-empty array");
PASS();
}
void test_task_contains_annotation_fields() {
TEST(task_contains_annotation_fields);
MCPServer mcp;
json intake = intakeOutput(mcp, clearSpec());
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
const auto& task = out["tasks"][0];
CHECK(task.contains("confidence"), "confidence missing");
CHECK(task.contains("ambiguityCount"), "ambiguityCount missing");
CHECK(task.contains("escalate"), "escalate missing");
CHECK(task.contains("reasons"), "reasons missing");
PASS();
}
void test_conflict_count_roundtrips_from_input() {
TEST(conflict_count_roundtrips_from_input);
MCPServer mcp;
json intake = intakeOutput(mcp, riskySpec());
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
CHECK(out.value("conflictCount", 0) >= 1, "expected conflict count >= 1");
PASS();
}
void test_risky_input_triggers_escalation() {
TEST(risky_input_triggers_escalation);
MCPServer mcp;
json intake = intakeOutput(mcp, riskySpec());
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
CHECK(out.value("escalateCount", 0) >= 1, "expected escalations");
PASS();
}
void test_clear_input_has_non_escalated_task() {
TEST(clear_input_has_non_escalated_task);
MCPServer mcp;
json intake = intakeOutput(mcp, clearSpec());
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
bool foundNonEscalated = false;
for (const auto& task : out["tasks"]) {
if (!task.value("escalate", true)) foundNonEscalated = true;
}
CHECK(foundNonEscalated, "expected at least one non-escalated task");
PASS();
}
void test_missing_normalized_requirements_errors() {
TEST(missing_normalized_requirements_errors);
MCPServer mcp;
json out = callTool(mcp, "whetstone_generate_taskitems", json::object());
CHECK(!out.value("success", true), "success should be false");
CHECK(out.value("error", "") == "normalized_requirements_missing", "wrong error");
PASS();
}
void test_normalized_requirements_must_be_array() {
TEST(normalized_requirements_must_be_array);
MCPServer mcp;
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", "bad"}});
CHECK(!out.value("success", true), "success should be false");
CHECK(out.value("error", "") == "normalized_requirements_not_array", "wrong error");
PASS();
}
void test_rejects_empty_normalized_requirements() {
TEST(rejects_empty_normalized_requirements);
MCPServer mcp;
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", json::array()}});
CHECK(!out.value("success", true), "success should be false");
CHECK(out.value("error", "") == "normalized_requirements_empty", "wrong error");
PASS();
}
void test_rejects_invalid_requirement_kind() {
TEST(rejects_invalid_requirement_kind);
MCPServer mcp;
json bad = json::array({{
{"requirementId", "goal-1"},
{"kind", "invalid-kind"},
{"normalizedText", "ship it"},
{"anchor", "goals"},
{"sourceLine", 2},
{"ambiguous", false}
}});
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", bad}});
CHECK(!out.value("success", true), "success should be false");
CHECK(out.value("error", "") == "requirement_kind_invalid", "wrong error");
PASS();
}
void test_cross_project_repo_signal_grounds_target_files() {
TEST(cross_project_repo_signal_grounds_target_files);
MCPServer mcp;
json reqs = json::array({{
{"requirementId", "goal-constcad"},
{"kind", "goal"},
{"normalizedText", "ConstCAD ecosystem integration across cad_orchestrator, hivemind, gleaner, and constcad"},
{"anchor", "goals"},
{"sourceLine", 1},
{"ambiguous", false}
}});
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", reqs}, {"strictExecutionContract", true}});
CHECK(out.value("success", false), "success should be true");
const auto& files = out["tasks"][0]["executionContract"]["targetFiles"];
bool hasExternal = false;
for (const auto& f : files) {
if (!f.is_string()) continue;
std::string v = f.get<std::string>();
if (v.rfind("cad_orchestrator/", 0) == 0 ||
v.rfind("hivemind/", 0) == 0 ||
v.rfind("gleaner/", 0) == 0 ||
v.rfind("constcad/", 0) == 0) {
hasExternal = true;
}
CHECK(v.rfind("editor/src/mcp/", 0) != 0, "should not fallback to editor/src/mcp internal defaults");
}
CHECK(hasExternal, "expected at least one cross-project target file");
PASS();
}
void test_project_requirement_generates_specific_task_title_and_files() {
TEST(project_requirement_generates_specific_task_title_and_files);
MCPServer mcp;
json reqs = json::array({{
{"requirementId", "goal-guidance-bridge"},
{"kind", "goal"},
{"normalizedText", "Implement GuidanceBridge formulas in WHIMP/software/cpp/src/guidance/GuidanceBridge.cpp and WHIMP/software/cpp/src/guidance/GuidanceBridge.h"},
{"anchor", "goals"},
{"sourceLine", 1},
{"ambiguous", false}
}});
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", reqs}, {"strictExecutionContract", true}});
CHECK(out.value("success", false), "success should be true");
CHECK(out["tasks"].is_array() && !out["tasks"].empty(), "tasks should be non-empty");
const auto& task = out["tasks"][0];
std::string title = task.value("title", "");
std::string intent = task.value("intent", "");
CHECK(title.find("GuidanceBridge") != std::string::npos, "title should mention GuidanceBridge");
CHECK(title.find("Intake Foundation") == std::string::npos, "title should not fallback to generic milestone wording");
CHECK(intent.find("GuidanceBridge") != std::string::npos, "intent should preserve requirement text");
const auto& files = task["executionContract"]["targetFiles"];
bool hasCpp = false;
bool hasHeader = false;
for (const auto& f : files) {
if (!f.is_string()) continue;
std::string v = f.get<std::string>();
if (v == "WHIMP/software/cpp/src/guidance/GuidanceBridge.cpp") hasCpp = true;
if (v == "WHIMP/software/cpp/src/guidance/GuidanceBridge.h") hasHeader = true;
}
CHECK(hasCpp, "expected cpp target file");
CHECK(hasHeader, "expected header target file");
PASS();
}
void test_single_project_product_name_does_not_trigger_cross_repo_fallback() {
TEST(single_project_product_name_does_not_trigger_cross_repo_fallback);
MCPServer mcp;
json reqs = json::array({{
{"requirementId", "goal-hivemind-shell-onboarding"},
{"kind", "goal"},
{"normalizedText", "Add onboarding plan for installing hivemind_shell in docs/onboarding/hivemind_shell_ubuntu.md"},
{"anchor", "requirements"},
{"sourceLine", 1},
{"ambiguous", false}
}});
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", reqs}, {"strictExecutionContract", true}});
CHECK(out.value("success", false), "success should be true");
CHECK(out["tasks"].is_array() && !out["tasks"].empty(), "tasks should be non-empty");
const auto& files = out["tasks"][0]["executionContract"]["targetFiles"];
bool hasLocalDoc = false;
for (const auto& f : files) {
if (!f.is_string()) continue;
std::string v = f.get<std::string>();
if (v == "docs/onboarding/hivemind_shell_ubuntu.md") hasLocalDoc = true;
CHECK(v != "hivemind/README.md", "single-project product name should not trigger cross-repo fallback");
}
CHECK(hasLocalDoc, "expected local onboarding doc target");
PASS();
}
void test_strict_contract_infers_test_files_and_verification_type_for_code_work() {
TEST(strict_contract_infers_test_files_and_verification_type_for_code_work);
MCPServer mcp;
json reqs = json::array({{
{"requirementId", "goal-guidance-bridge-tests"},
{"kind", "goal"},
{"normalizedText", "Implement GuidanceBridge formulas in WHIMP/software/cpp/src/guidance/GuidanceBridge.cpp and WHIMP/software/cpp/src/guidance/GuidanceBridge.h"},
{"anchor", "goals"},
{"sourceLine", 1},
{"ambiguous", false}
}});
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", reqs}, {"strictExecutionContract", true}});
CHECK(out.value("success", false), "success should be true");
CHECK(out["tasks"].is_array() && !out["tasks"].empty(), "tasks should be non-empty");
const auto& contract = out["tasks"][0]["executionContract"];
CHECK(contract.value("verificationType", "") == "unit", "expected unit verification type");
const auto& tests = contract["testFiles"];
CHECK(tests.is_array() && !tests.empty(), "expected inferred test files");
bool hasGuidanceBridgeTest = false;
for (const auto& testFile : tests) {
if (!testFile.is_string()) continue;
if (testFile.get<std::string>() == "WHIMP/software/cpp/tests/guidance/GuidanceBridge_test.cpp") {
hasGuidanceBridgeTest = true;
}
}
CHECK(hasGuidanceBridgeTest, "expected inferred GuidanceBridge C++ test path");
PASS();
}
void test_rust_repo_contract_drops_wrong_language_commands_and_uses_real_test_file() {
TEST(rust_repo_contract_drops_wrong_language_commands_and_uses_real_test_file);
MCPServer mcp;
json reqs = json::array({
{
{"requirementId", "goal-rust-1"},
{"kind", "goal"},
{"normalizedText", "Implement control-plane recomposition in crates/whimpwm/src/render.rs and crates/whimpwm/src/presentation.rs"},
{"sourceText", "Implement control-plane recomposition in `crates/whimpwm/src/render.rs` and `crates/whimpwm/src/presentation.rs`"},
{"anchor", "goals"},
{"sourceLine", 1},
{"ambiguous", false}
},
{
{"requirementId", "constraint-rust-1"},
{"kind", "constraint"},
{"normalizedText", "Do not generate or verify go tasks for this sprint"},
{"sourceText", "Do not generate or verify Go tasks for this sprint."},
{"anchor", "constraints"},
{"sourceLine", 2},
{"ambiguous", false}
}
});
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", reqs},
{"strictExecutionContract", true},
{"workspace", "/home/bill/Documents/whimpwm"}});
CHECK(out.value("success", false), "success should be true");
CHECK(out["tasks"].is_array() && !out["tasks"].empty(), "tasks should be non-empty");
const auto& contract = out["tasks"][0]["executionContract"];
bool sawGo = false;
bool sawCargo = false;
for (const auto& commandJson : contract["acceptanceCommands"]) {
if (!commandJson.is_string()) continue;
const std::string command = commandJson.get<std::string>();
if (command.find("go test") != std::string::npos) sawGo = true;
if (command.find("cargo ") != std::string::npos) sawCargo = true;
}
CHECK(!sawGo, "go test should be removed for Rust repo");
CHECK(sawCargo, "cargo command should be present for Rust repo");
bool sawMainTest = false;
for (const auto& testFileJson : contract["testFiles"]) {
if (!testFileJson.is_string()) continue;
if (testFileJson.get<std::string>() == "crates/whimpwm/tests/main_test.rs") sawMainTest = true;
}
CHECK(sawMainTest, "expected real whimpwm Rust test file");
PASS();
}
int main() {
std::cout << "Step 615: whetstone_generate_taskitems MCP Tool\n";
test_tool_registered_accessor(); // 1
test_tool_schema_requires_normalized_requirements(); // 2
test_success_for_intake_output(); // 3
test_returns_non_empty_tasks(); // 4
test_task_contains_annotation_fields(); // 5
test_conflict_count_roundtrips_from_input(); // 6
test_risky_input_triggers_escalation(); // 7
test_clear_input_has_non_escalated_task(); // 8
test_missing_normalized_requirements_errors(); // 9
test_normalized_requirements_must_be_array(); // 10
test_rejects_empty_normalized_requirements(); // 11
test_rejects_invalid_requirement_kind(); // 12
test_cross_project_repo_signal_grounds_target_files(); // 13
test_project_requirement_generates_specific_task_title_and_files(); // 14
test_single_project_product_name_does_not_trigger_cross_repo_fallback(); // 15
test_strict_contract_infers_test_files_and_verification_type_for_code_work(); // 16
test_rust_repo_contract_drops_wrong_language_commands_and_uses_real_test_file(); // 17
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed == 0 ? 0 : 1;
}