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>
374 lines
14 KiB
C++
374 lines
14 KiB
C++
// Step 263 TDD Test: Save Buffer to Disk
|
|
//
|
|
// Tests saveBuffer (regenerate code from AST, write to disk, clear modified)
|
|
// and saveAllBuffers (batch save all modified buffers). Both respect the
|
|
// workspace root for path resolution.
|
|
#include "HeadlessEditorState.h"
|
|
#include "MCPServer.h"
|
|
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
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);
|
|
}
|
|
|
|
// Helper: read file content from disk
|
|
static std::string readFile(const std::string& path) {
|
|
std::ifstream f(path);
|
|
if (!f.is_open()) return "";
|
|
std::string content((std::istreambuf_iterator<char>(f)),
|
|
std::istreambuf_iterator<char>());
|
|
return content;
|
|
}
|
|
|
|
int main() {
|
|
int passed = 0;
|
|
int failed = 0;
|
|
|
|
// Create a temp directory for save tests
|
|
fs::path tmpDir = fs::temp_directory_path() / "whetstone_step263_test";
|
|
fs::create_directories(tmpDir);
|
|
|
|
std::string workspace = tmpDir.string();
|
|
|
|
std::string srcA =
|
|
"def greet(name):\n"
|
|
" return \"hello \" + name\n";
|
|
|
|
std::string srcB =
|
|
"def add(x, y):\n"
|
|
" return x + y\n";
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 1: saveBuffer writes editBuf to disk
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "greet.py"}, {"content", srcA}});
|
|
|
|
json resp = rpc(state, "s1", "saveBuffer",
|
|
{{"path", "greet.py"}});
|
|
bool success = resp.contains("result") &&
|
|
resp["result"].value("success", false);
|
|
int bytes = resp["result"].value("bytesWritten", 0);
|
|
|
|
// Verify file on disk
|
|
std::string onDisk = readFile((tmpDir / "greet.py").string());
|
|
bool written = !onDisk.empty();
|
|
|
|
expect(success && bytes > 0 && written,
|
|
"saveBuffer writes file to disk (" +
|
|
std::to_string(bytes) + " bytes)",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 2: saveBuffer clears the modified flag
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "mod.py"}, {"content", srcA}});
|
|
|
|
// Mark as modified (simulating a mutation)
|
|
state.bufferStates["mod.py"]->modified = true;
|
|
bool wasMod = state.bufferStates["mod.py"]->modified;
|
|
|
|
rpc(state, "s1", "saveBuffer", {{"path", "mod.py"}});
|
|
|
|
bool nowClean = !state.bufferStates["mod.py"]->modified;
|
|
expect(wasMod && nowClean,
|
|
"saveBuffer clears modified flag",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 3: saveBuffer returns correct response fields
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "fields.py"}, {"content", srcA}});
|
|
|
|
json resp = rpc(state, "s1", "saveBuffer",
|
|
{{"path", "fields.py"}});
|
|
bool hasPath = resp["result"].contains("path");
|
|
bool hasBytes = resp["result"].contains("bytesWritten");
|
|
bool hasSuccess = resp["result"].contains("success");
|
|
expect(hasPath && hasBytes && hasSuccess,
|
|
"Response has path, bytesWritten, success fields",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 4: saveAllBuffers saves all modified, skips unmodified
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "a.py"}, {"content", srcA}});
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "b.py"}, {"content", srcB}});
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "c.py"}, {"content", srcA}});
|
|
|
|
// Mark only a.py and c.py as modified
|
|
state.bufferStates["a.py"]->modified = true;
|
|
state.bufferStates["c.py"]->modified = true;
|
|
// b.py stays unmodified
|
|
|
|
json resp = rpc(state, "s1", "saveAllBuffers");
|
|
int savedCount = resp["result"].value("savedCount", 0);
|
|
int skippedCount = resp["result"].value("skippedCount", 0);
|
|
|
|
expect(savedCount == 2 && skippedCount == 1,
|
|
"saveAllBuffers: saved=" + std::to_string(savedCount)
|
|
+ " skipped=" + std::to_string(skippedCount),
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 5: saveBuffer after mutation — regenerated code on disk
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "rename_save.py"}, {"content", srcA}});
|
|
|
|
// Rename the function
|
|
rpc(state, "s1", "renameSymbol",
|
|
{{"oldName", "greet"}, {"newName", "salute"}});
|
|
|
|
// Save to disk
|
|
rpc(state, "s1", "saveBuffer",
|
|
{{"path", "rename_save.py"}});
|
|
|
|
// Read from disk and verify the rename
|
|
std::string onDisk = readFile(
|
|
(tmpDir / "rename_save.py").string());
|
|
bool hasSalute = onDisk.find("salute") != std::string::npos;
|
|
bool noGreet = onDisk.find("greet") == std::string::npos;
|
|
|
|
expect(hasSalute && noGreet,
|
|
"Saved file contains renamed function 'salute'",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 6: saveBuffer requires Refactor or Generator role
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("admin", AgentRole::Refactor);
|
|
state.setAgentRole("linter", AgentRole::Linter);
|
|
|
|
rpc(state, "admin", "openFile",
|
|
{{"path", "perm.py"}, {"content", srcA}});
|
|
|
|
// Linter cannot save
|
|
json denied = rpc(state, "linter", "saveBuffer",
|
|
{{"path", "perm.py"}});
|
|
bool linterDenied = denied.contains("error");
|
|
|
|
// Refactor can save
|
|
json allowed = rpc(state, "admin", "saveBuffer",
|
|
{{"path", "perm.py"}});
|
|
bool refactorOk = allowed.contains("result");
|
|
|
|
expect(linterDenied && refactorOk,
|
|
"Linter denied, Refactor allowed for saveBuffer",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 7: saveBuffer on active buffer (no path param)
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "active_save.py"}, {"content", srcA}});
|
|
state.bufferStates["active_save.py"]->modified = true;
|
|
|
|
// Save without specifying path — should save active buffer
|
|
json resp = rpc(state, "s1", "saveBuffer");
|
|
bool success = resp.contains("result") &&
|
|
resp["result"].value("success", false);
|
|
|
|
// Verify file on disk
|
|
std::string onDisk = readFile(
|
|
(tmpDir / "active_save.py").string());
|
|
bool written = !onDisk.empty();
|
|
|
|
expect(success && written,
|
|
"saveBuffer without path saves active buffer",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 8: saveBuffer error on unknown buffer
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
json resp = rpc(state, "s1", "saveBuffer",
|
|
{{"path", "nonexistent.py"}});
|
|
bool hasError = resp.contains("error");
|
|
expect(hasError,
|
|
"saveBuffer on unknown buffer returns error",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 9: saveAllBuffers clears modified flags on all saved
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "x.py"}, {"content", srcA}});
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "y.py"}, {"content", srcB}});
|
|
state.bufferStates["x.py"]->modified = true;
|
|
state.bufferStates["y.py"]->modified = true;
|
|
|
|
rpc(state, "s1", "saveAllBuffers");
|
|
|
|
bool xClean = !state.bufferStates["x.py"]->modified;
|
|
bool yClean = !state.bufferStates["y.py"]->modified;
|
|
expect(xClean && yClean,
|
|
"saveAllBuffers clears modified flags on all saved",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 10: saveAllBuffers returns saved file paths
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "p.py"}, {"content", srcA}});
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "q.py"}, {"content", srcB}});
|
|
state.bufferStates["p.py"]->modified = true;
|
|
state.bufferStates["q.py"]->modified = true;
|
|
|
|
json resp = rpc(state, "s1", "saveAllBuffers");
|
|
bool hasSaved = resp["result"].contains("saved") &&
|
|
resp["result"]["saved"].is_array();
|
|
int savedSize = hasSaved ? (int)resp["result"]["saved"].size() : 0;
|
|
expect(hasSaved && savedSize == 2,
|
|
"saveAllBuffers returns saved file paths (" +
|
|
std::to_string(savedSize) + ")",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 11: MCP tools registered
|
|
// ---------------------------------------------------------------
|
|
{
|
|
MCPServer mcp;
|
|
bool foundSave = false;
|
|
bool foundSaveAll = false;
|
|
for (const auto& t : mcp.getTools()) {
|
|
if (t.name == "whetstone_save_buffer") foundSave = true;
|
|
if (t.name == "whetstone_save_all_buffers")
|
|
foundSaveAll = true;
|
|
}
|
|
expect(foundSave && foundSaveAll,
|
|
"whetstone_save_buffer and whetstone_save_all_buffers "
|
|
"MCP tools registered",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 12: saveBuffer with workspace path resolution
|
|
// ---------------------------------------------------------------
|
|
{
|
|
HeadlessEditorState state;
|
|
state.defaultLanguage = "python";
|
|
state.workspaceRoot = workspace;
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
rpc(state, "s1", "openFile",
|
|
{{"path", "sub/deep.py"}, {"content", srcA}});
|
|
state.bufferStates["sub/deep.py"]->modified = true;
|
|
|
|
json resp = rpc(state, "s1", "saveBuffer",
|
|
{{"path", "sub/deep.py"}});
|
|
bool success = resp.contains("result") &&
|
|
resp["result"].value("success", false);
|
|
|
|
// Verify the subdirectory was created
|
|
bool fileExists = fs::exists(tmpDir / "sub" / "deep.py");
|
|
expect(success && fileExists,
|
|
"saveBuffer creates parent dirs and writes file",
|
|
passed, failed);
|
|
}
|
|
|
|
// Cleanup
|
|
fs::remove_all(tmpDir);
|
|
|
|
std::cout << "\n=== Step 263 Results: " << passed << " passed, "
|
|
<< failed << " failed ===\n";
|
|
return failed == 0 ? 0 : 1;
|
|
}
|