Files
whetstone_DSL/editor/tests/step264_test.cpp
Bill c36fd92045 Steps 263-265: Phase 9e — save buffer to disk, undo/redo, integration tests
Step 263: saveBuffer/saveAllBuffers RPC methods write editBuf to disk with
workspace path resolution and parent dir creation. 12/12 tests.

Step 264: State-based undo/redo via HeadlessUndoStack. Snapshots recorded
automatically before mutations; undo/redo restore AST + editBuf. 12/12 tests.

Step 265: Phase 9e integration tests — save after undo/redo, mixed undo
states across buffers, full open→mutate→diagnose→undo→redo→save. 8/8 tests.

Phase 9e complete: 32/32 tests. Sprint 9 total: 244/244 across 5 phases.
34 MCP tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:27:23 +00:00

409 lines
14 KiB
C++

// Step 264 TDD Test: Undo/Redo for Headless Mode
//
// Tests undo and redo RPC methods that use the Orchestrator's snapshot
// journal to restore previous/next AST + editBuf states after mutations.
// Snapshots are recorded automatically before each mutation.
#include "HeadlessEditorState.h"
#include "MCPServer.h"
#include <iostream>
#include <string>
static void expect(bool cond, const std::string& name,
int& passed, int& failed) {
if (cond) {
std::cout << "Test " << (passed + failed + 1)
<< " PASS: " << name << "\n";
++passed;
} else {
std::cout << "Test " << (passed + failed + 1)
<< " FAIL: " << name << "\n";
++failed;
}
}
static json rpc(HeadlessEditorState& state, const std::string& session,
const std::string& method,
json params = json::object()) {
json request = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", method}, {"params", params}};
return state.processAgentRequest(request, session);
}
int main() {
int passed = 0;
int failed = 0;
std::string src =
"def greet(name):\n"
" return \"hello \" + name\n";
// ---------------------------------------------------------------
// Test 1: undo with no history returns error
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
json resp = rpc(state, "s1", "undo");
bool hasError = resp.contains("error");
expect(hasError,
"undo with no mutation history returns error",
passed, failed);
}
// ---------------------------------------------------------------
// Test 2: mutate → undo restores previous AST
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
// Rename greet → salute
rpc(state, "s1", "renameSymbol",
{{"oldName", "greet"}, {"newName", "salute"}});
// Verify salute exists
bool hasSalute = false;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" &&
getNodeName(c) == "salute")
hasSalute = true;
}
// Undo — should restore greet
json resp = rpc(state, "s1", "undo");
bool undoOk = resp.contains("result") &&
resp["result"].value("success", false);
bool hasGreet = false;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" &&
getNodeName(c) == "greet")
hasGreet = true;
}
expect(hasSalute && undoOk && hasGreet,
"rename→undo restores original function name",
passed, failed);
}
// ---------------------------------------------------------------
// Test 3: undo restores editBuf (regenerated code)
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
std::string beforeCode = state.activeBuffer->editBuf;
rpc(state, "s1", "renameSymbol",
{{"oldName", "greet"}, {"newName", "salute"}});
std::string afterCode = state.activeBuffer->editBuf;
rpc(state, "s1", "undo");
std::string undoneCode = state.activeBuffer->editBuf;
bool codeChanged = afterCode != beforeCode;
bool codeRestored = undoneCode.find("greet") != std::string::npos;
expect(codeChanged && codeRestored,
"undo restores editBuf with original code",
passed, failed);
}
// ---------------------------------------------------------------
// Test 4: undo → redo restores the mutation
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
rpc(state, "s1", "renameSymbol",
{{"oldName", "greet"}, {"newName", "salute"}});
rpc(state, "s1", "undo");
// Now redo
json resp = rpc(state, "s1", "redo");
bool redoOk = resp.contains("result") &&
resp["result"].value("success", false);
bool hasSalute = false;
if (state.activeAST()) {
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" &&
getNodeName(c) == "salute")
hasSalute = true;
}
}
expect(redoOk && hasSalute,
"undo→redo restores the renamed function",
passed, failed);
}
// ---------------------------------------------------------------
// Test 5: redo with nothing to redo returns error
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
json resp = rpc(state, "s1", "redo");
bool hasError = resp.contains("error");
expect(hasError,
"redo with nothing to redo returns error",
passed, failed);
}
// ---------------------------------------------------------------
// Test 6: undo/redo report depths
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
// Two mutations
rpc(state, "s1", "renameSymbol",
{{"oldName", "greet"}, {"newName", "salute"}});
rpc(state, "s1", "renameSymbol",
{{"oldName", "salute"}, {"newName", "welcome"}});
// Undo once
json resp1 = rpc(state, "s1", "undo");
int undoDepth1 = resp1["result"].value("undoDepth", -1);
int redoDepth1 = resp1["result"].value("redoDepth", -1);
// Undo again
json resp2 = rpc(state, "s1", "undo");
int undoDepth2 = resp2["result"].value("undoDepth", -1);
int redoDepth2 = resp2["result"].value("redoDepth", -1);
expect(undoDepth1 == 1 && redoDepth1 == 1 &&
undoDepth2 == 0 && redoDepth2 == 2,
"Depths: after 1st undo=" + std::to_string(undoDepth1) +
"/" + std::to_string(redoDepth1) + ", after 2nd undo=" +
std::to_string(undoDepth2) + "/" +
std::to_string(redoDepth2),
passed, failed);
}
// ---------------------------------------------------------------
// Test 7: Linter cannot undo/redo
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("admin", AgentRole::Refactor);
state.setAgentRole("linter", AgentRole::Linter);
rpc(state, "admin", "openFile",
{{"path", "t.py"}, {"content", src}});
rpc(state, "admin", "renameSymbol",
{{"oldName", "greet"}, {"newName", "salute"}});
json undoResp = rpc(state, "linter", "undo");
json redoResp = rpc(state, "linter", "redo");
bool undoDenied = undoResp.contains("error");
bool redoDenied = redoResp.contains("error");
expect(undoDenied && redoDenied,
"Linter cannot undo or redo",
passed, failed);
}
// ---------------------------------------------------------------
// Test 8: multiple mutations → multiple undos
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
// Three renames
rpc(state, "s1", "renameSymbol",
{{"oldName", "greet"}, {"newName", "a"}});
rpc(state, "s1", "renameSymbol",
{{"oldName", "a"}, {"newName", "b"}});
rpc(state, "s1", "renameSymbol",
{{"oldName", "b"}, {"newName", "c"}});
// Function should be "c" now
bool isC = false;
for (auto* ch : state.activeAST()->allChildren()) {
if (ch->conceptType == "Function" && getNodeName(ch) == "c")
isC = true;
}
// Undo 3 times — back to greet
rpc(state, "s1", "undo");
rpc(state, "s1", "undo");
rpc(state, "s1", "undo");
bool isGreet = false;
for (auto* ch : state.activeAST()->allChildren()) {
if (ch->conceptType == "Function" &&
getNodeName(ch) == "greet")
isGreet = true;
}
expect(isC && isGreet,
"3 mutations → 3 undos restores original",
passed, failed);
}
// ---------------------------------------------------------------
// Test 9: new mutation after undo clears redo stack
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
rpc(state, "s1", "renameSymbol",
{{"oldName", "greet"}, {"newName", "a"}});
rpc(state, "s1", "undo");
// Redo should be available
int redoBefore = state.activeBuffer->undoStack.redoDepth();
// New mutation should clear redo
rpc(state, "s1", "renameSymbol",
{{"oldName", "greet"}, {"newName", "z"}});
int redoAfter = state.activeBuffer->undoStack.redoDepth();
// Redo should now be empty
json redoResp = rpc(state, "s1", "redo");
bool redoFails = redoResp.contains("error");
expect(redoBefore == 1 && redoAfter == 0 && redoFails,
"New mutation after undo clears redo stack",
passed, failed);
}
// ---------------------------------------------------------------
// Test 10: undo sets modified flag
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
state.activeBuffer->modified = false;
rpc(state, "s1", "renameSymbol",
{{"oldName", "greet"}, {"newName", "salute"}});
state.activeBuffer->modified = false; // simulate save
rpc(state, "s1", "undo");
bool modAfterUndo = state.activeBuffer->modified;
expect(modAfterUndo,
"undo sets modified flag on buffer",
passed, failed);
}
// ---------------------------------------------------------------
// Test 11: MCP tools registered
// ---------------------------------------------------------------
{
MCPServer mcp;
bool foundUndo = false;
bool foundRedo = false;
for (const auto& t : mcp.getTools()) {
if (t.name == "whetstone_undo") foundUndo = true;
if (t.name == "whetstone_redo") foundRedo = true;
}
expect(foundUndo && foundRedo,
"whetstone_undo and whetstone_redo MCP tools registered",
passed, failed);
}
// ---------------------------------------------------------------
// Test 12: undo/redo with applyMutation (not just rename)
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
// Get function node ID
std::string fnId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" &&
getNodeName(c) == "greet") {
fnId = c->id;
break;
}
}
// Mutate via setProperty
rpc(state, "s1", "applyMutation",
{{"type", "setProperty"}, {"nodeId", fnId},
{"property", "name"}, {"value", "howdy"}});
bool isHowdy = false;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" &&
getNodeName(c) == "howdy")
isHowdy = true;
}
// Undo
rpc(state, "s1", "undo");
bool isGreetAgain = false;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" &&
getNodeName(c) == "greet")
isGreetAgain = true;
}
expect(isHowdy && isGreetAgain,
"applyMutation→undo restores original name",
passed, failed);
}
std::cout << "\n=== Step 264 Results: " << passed << " passed, "
<< failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}