Phase 3e complete: Agent API Extend (Steps 59, 61-63)
Step 59: WebSocketAgentServer with session management, JSON-RPC routing, MockWebSocketTransport (10/10 tests) Step 61: ASTMutationAPI with setProperty, updateNode, deleteNode, insertNode, journal, OptimizationLock warning (6/6 tests) Step 62: ContextAPI with getInScopeSymbols, getCallHierarchy, getDependencyGraph (6/6 tests) Step 63: BatchMutationAPI with atomic applySequence and reverse-order rollback (5/5 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -265,6 +265,10 @@ target_include_directories(step57_test PRIVATE src)
|
||||
add_executable(step58_test tests/step58_test.cpp)
|
||||
target_include_directories(step58_test PRIVATE src)
|
||||
|
||||
add_executable(step59_test tests/step59_test.cpp)
|
||||
target_include_directories(step59_test PRIVATE src)
|
||||
target_link_libraries(step59_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step60_test tests/step60_test.cpp)
|
||||
target_include_directories(step60_test PRIVATE src)
|
||||
target_link_libraries(step60_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
215
editor/src/ASTMutationAPI.h
Normal file
215
editor/src/ASTMutationAPI.h
Normal file
@@ -0,0 +1,215 @@
|
||||
#pragma once
|
||||
// Step 61: AST mutation API (extend)
|
||||
//
|
||||
// ASTMutationAPI: provides validated AST mutations with journal recording
|
||||
// and OptimizationLock awareness.
|
||||
//
|
||||
// Methods:
|
||||
// setProperty — set a single property on a node
|
||||
// updateNode — bulk property update
|
||||
// deleteNode — remove a node from its parent
|
||||
// insertNode — add a node as child of a parent in a given role
|
||||
//
|
||||
// All mutations check for OptimizationLock annotations on the target
|
||||
// node or its ancestors. If a lock is found the mutation still succeeds
|
||||
// but a warning is returned (per spec: "warn, don't reject").
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Module.h"
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
#include "ast/Parameter.h"
|
||||
#include "ast/Expression.h"
|
||||
#include "ast/Annotation.h"
|
||||
|
||||
class ASTMutationAPI {
|
||||
public:
|
||||
struct MutationResult {
|
||||
bool success = false;
|
||||
std::string warning; // non-empty if a warning was produced
|
||||
std::string error; // non-empty if the mutation failed
|
||||
};
|
||||
|
||||
void setRoot(ASTNode* root) { root_ = root; }
|
||||
|
||||
// --- mutations ---
|
||||
|
||||
MutationResult setProperty(const std::string& nodeId,
|
||||
const std::string& property,
|
||||
const std::string& value) {
|
||||
MutationResult res;
|
||||
ASTNode* node = findById(root_, nodeId);
|
||||
if (!node) {
|
||||
res.error = "Node not found: " + nodeId;
|
||||
return res;
|
||||
}
|
||||
|
||||
// Lock check
|
||||
std::string lockWarning = checkLock(node);
|
||||
|
||||
if (!applyProperty(node, property, value)) {
|
||||
res.error = "Cannot set property '" + property +
|
||||
"' on " + node->conceptType;
|
||||
return res;
|
||||
}
|
||||
|
||||
// Record in journal
|
||||
journal_.push_back("setProperty " + nodeId + "." + property +
|
||||
" = " + value);
|
||||
|
||||
res.success = true;
|
||||
res.warning = lockWarning;
|
||||
return res;
|
||||
}
|
||||
|
||||
MutationResult updateNode(const std::string& nodeId,
|
||||
const std::map<std::string, std::string>& properties) {
|
||||
MutationResult res;
|
||||
ASTNode* node = findById(root_, nodeId);
|
||||
if (!node) {
|
||||
res.error = "Node not found: " + nodeId;
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string lockWarning = checkLock(node);
|
||||
|
||||
for (const auto& [prop, val] : properties) {
|
||||
if (!applyProperty(node, prop, val)) {
|
||||
res.error = "Cannot set property '" + prop +
|
||||
"' on " + node->conceptType;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
journal_.push_back("updateNode " + nodeId + " (" +
|
||||
std::to_string(properties.size()) + " properties)");
|
||||
|
||||
res.success = true;
|
||||
res.warning = lockWarning;
|
||||
return res;
|
||||
}
|
||||
|
||||
MutationResult deleteNode(const std::string& nodeId) {
|
||||
MutationResult res;
|
||||
ASTNode* node = findById(root_, nodeId);
|
||||
if (!node) {
|
||||
res.error = "Node not found: " + nodeId;
|
||||
return res;
|
||||
}
|
||||
if (!node->parent) {
|
||||
res.error = "Cannot delete root node";
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string lockWarning = checkLock(node);
|
||||
|
||||
ASTNode* par = node->parent;
|
||||
if (!par->removeChild(node)) {
|
||||
res.error = "Failed to remove node from parent";
|
||||
return res;
|
||||
}
|
||||
|
||||
journal_.push_back("deleteNode " + nodeId);
|
||||
|
||||
res.success = true;
|
||||
res.warning = lockWarning;
|
||||
return res;
|
||||
}
|
||||
|
||||
MutationResult insertNode(const std::string& parentId,
|
||||
const std::string& role,
|
||||
ASTNode* node) {
|
||||
MutationResult res;
|
||||
ASTNode* parent = findById(root_, parentId);
|
||||
if (!parent) {
|
||||
res.error = "Parent not found: " + parentId;
|
||||
return res;
|
||||
}
|
||||
if (!node) {
|
||||
res.error = "Node is null";
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string lockWarning = checkLock(parent);
|
||||
|
||||
parent->addChild(role, node);
|
||||
|
||||
journal_.push_back("insertNode " + node->id + " into " +
|
||||
parentId + "." + role);
|
||||
|
||||
res.success = true;
|
||||
res.warning = lockWarning;
|
||||
return res;
|
||||
}
|
||||
|
||||
// --- journal ---
|
||||
|
||||
std::vector<std::string> getJournalEntries() const { return journal_; }
|
||||
void clearJournal() { journal_.clear(); }
|
||||
|
||||
private:
|
||||
// Recursive node lookup
|
||||
ASTNode* findById(ASTNode* node, const std::string& nodeId) const {
|
||||
if (!node) return nullptr;
|
||||
if (node->id == nodeId) return node;
|
||||
for (auto* child : node->allChildren()) {
|
||||
auto* found = findById(child, nodeId);
|
||||
if (found) return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Walk up the ancestor chain looking for an OptimizationLock annotation.
|
||||
// Returns a warning string if locked, empty string otherwise.
|
||||
std::string checkLock(const ASTNode* node) const {
|
||||
const ASTNode* cur = node;
|
||||
while (cur) {
|
||||
for (const auto* anno : cur->getChildren("annotations")) {
|
||||
if (anno->conceptType == "OptimizationLock") {
|
||||
auto* lock = static_cast<const OptimizationLock*>(anno);
|
||||
return "Node is under OptimizationLock (locked by " +
|
||||
lock->lockedBy + ": " + lock->lockReason + ")";
|
||||
}
|
||||
}
|
||||
cur = cur->parent;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Apply a string property to a node. Returns false if the property
|
||||
// is not recognised for the node's concept type.
|
||||
bool applyProperty(ASTNode* node, const std::string& property,
|
||||
const std::string& value) const {
|
||||
const auto& ct = node->conceptType;
|
||||
if (property == "name") {
|
||||
if (ct == "Function") { static_cast<Function*>(node)->name = value; return true; }
|
||||
if (ct == "Variable") { static_cast<Variable*>(node)->name = value; return true; }
|
||||
if (ct == "Parameter") { static_cast<Parameter*>(node)->name = value; return true; }
|
||||
if (ct == "Module") { static_cast<Module*>(node)->name = value; return true; }
|
||||
}
|
||||
if (property == "strategy") {
|
||||
if (ct == "DerefStrategy") { static_cast<DerefStrategy*>(node)->strategy = value; return true; }
|
||||
if (ct == "DeallocateAnnotation") { static_cast<DeallocateAnnotation*>(node)->strategy = value; return true; }
|
||||
if (ct == "LifetimeAnnotation") { static_cast<LifetimeAnnotation*>(node)->strategy = value; return true; }
|
||||
if (ct == "ReclaimAnnotation") { static_cast<ReclaimAnnotation*>(node)->strategy = value; return true; }
|
||||
if (ct == "OwnerAnnotation") { static_cast<OwnerAnnotation*>(node)->strategy = value; return true; }
|
||||
if (ct == "AllocateAnnotation") { static_cast<AllocateAnnotation*>(node)->strategy = value; return true; }
|
||||
}
|
||||
if (property == "op") {
|
||||
if (ct == "BinaryOperation") { static_cast<BinaryOperation*>(node)->op = value; return true; }
|
||||
}
|
||||
if (property == "variableName") {
|
||||
if (ct == "VariableReference") { static_cast<VariableReference*>(node)->variableName = value; return true; }
|
||||
}
|
||||
if (property == "targetLanguage") {
|
||||
if (ct == "Module") { static_cast<Module*>(node)->targetLanguage = value; return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ASTNode* root_ = nullptr;
|
||||
std::vector<std::string> journal_;
|
||||
};
|
||||
237
editor/src/BatchMutationAPI.h
Normal file
237
editor/src/BatchMutationAPI.h
Normal file
@@ -0,0 +1,237 @@
|
||||
#pragma once
|
||||
// Step 63: Batch operations (extend)
|
||||
//
|
||||
// BatchMutationAPI: atomic all-or-nothing mutation sequences.
|
||||
//
|
||||
// applySequence(mutations) tries each mutation in order. If any
|
||||
// mutation fails, every preceding mutation is rolled back in reverse
|
||||
// order so the AST returns to its pre-batch state.
|
||||
//
|
||||
// Supported mutation types: "setProperty", "insertNode", "deleteNode".
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Module.h"
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
#include "ast/Parameter.h"
|
||||
#include "ast/Expression.h"
|
||||
|
||||
class BatchMutationAPI {
|
||||
public:
|
||||
struct Mutation {
|
||||
std::string type; // "setProperty", "insertNode", "deleteNode"
|
||||
std::string nodeId;
|
||||
std::string property; // for setProperty
|
||||
std::string value; // for setProperty
|
||||
std::string parentId; // for insertNode
|
||||
std::string role; // for insertNode
|
||||
ASTNode* newNode = nullptr; // for insertNode
|
||||
};
|
||||
|
||||
struct BatchResult {
|
||||
bool success = false;
|
||||
int appliedCount = 0;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
void setRoot(ASTNode* root) { root_ = root; }
|
||||
|
||||
BatchResult applySequence(const std::vector<Mutation>& mutations) {
|
||||
BatchResult res;
|
||||
std::vector<UndoAction> undoStack;
|
||||
|
||||
for (size_t i = 0; i < mutations.size(); ++i) {
|
||||
const auto& mut = mutations[i];
|
||||
std::string err;
|
||||
|
||||
if (mut.type == "setProperty") {
|
||||
err = applySetProperty(mut, undoStack);
|
||||
} else if (mut.type == "insertNode") {
|
||||
err = applyInsertNode(mut, undoStack);
|
||||
} else if (mut.type == "deleteNode") {
|
||||
err = applyDeleteNode(mut, undoStack);
|
||||
} else {
|
||||
err = "Unknown mutation type: " + mut.type;
|
||||
}
|
||||
|
||||
if (!err.empty()) {
|
||||
// Roll back everything applied so far
|
||||
rollback(undoStack);
|
||||
res.success = false;
|
||||
res.appliedCount = static_cast<int>(i);
|
||||
res.error = err;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// All succeeded — record in journal
|
||||
for (const auto& undo : undoStack) {
|
||||
journal_.push_back(undo.description);
|
||||
}
|
||||
|
||||
res.success = true;
|
||||
res.appliedCount = static_cast<int>(mutations.size());
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<std::string> getJournalEntries() const { return journal_; }
|
||||
void clearJournal() { journal_.clear(); }
|
||||
|
||||
private:
|
||||
// An undo action captures how to reverse a single mutation.
|
||||
struct UndoAction {
|
||||
std::function<void()> undo;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
// --- mutation implementations ---
|
||||
|
||||
std::string applySetProperty(const Mutation& mut,
|
||||
std::vector<UndoAction>& undoStack) {
|
||||
ASTNode* node = findById(root_, mut.nodeId);
|
||||
if (!node)
|
||||
return "Node not found: " + mut.nodeId;
|
||||
|
||||
// Read old value
|
||||
std::string oldValue = getStringProperty(node, mut.property);
|
||||
|
||||
if (!setStringProperty(node, mut.property, mut.value))
|
||||
return "Cannot set property '" + mut.property +
|
||||
"' on " + node->conceptType;
|
||||
|
||||
// Capture undo
|
||||
std::string nid = mut.nodeId;
|
||||
std::string prop = mut.property;
|
||||
ASTNode** rootPtr = &root_;
|
||||
undoStack.push_back({
|
||||
[node, prop, oldValue, this]() {
|
||||
setStringProperty(node, prop, oldValue);
|
||||
},
|
||||
"setProperty " + mut.nodeId + "." + mut.property + " = " + mut.value
|
||||
});
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string applyInsertNode(const Mutation& mut,
|
||||
std::vector<UndoAction>& undoStack) {
|
||||
ASTNode* parent = findById(root_, mut.parentId);
|
||||
if (!parent)
|
||||
return "Parent not found: " + mut.parentId;
|
||||
if (!mut.newNode)
|
||||
return "Node is null for insertNode";
|
||||
|
||||
parent->addChild(mut.role, mut.newNode);
|
||||
|
||||
ASTNode* inserted = mut.newNode;
|
||||
undoStack.push_back({
|
||||
[parent, inserted]() {
|
||||
parent->removeChild(inserted);
|
||||
},
|
||||
"insertNode " + inserted->id + " into " + mut.parentId + "." + mut.role
|
||||
});
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string applyDeleteNode(const Mutation& mut,
|
||||
std::vector<UndoAction>& undoStack) {
|
||||
ASTNode* node = findById(root_, mut.nodeId);
|
||||
if (!node)
|
||||
return "Node not found: " + mut.nodeId;
|
||||
if (!node->parent)
|
||||
return "Cannot delete root node";
|
||||
|
||||
ASTNode* parent = node->parent;
|
||||
std::string role = findRole(parent, node);
|
||||
if (role.empty())
|
||||
return "Could not determine role for node " + mut.nodeId;
|
||||
|
||||
if (!parent->removeChild(node))
|
||||
return "Failed to remove node from parent";
|
||||
|
||||
undoStack.push_back({
|
||||
[parent, node, role]() {
|
||||
parent->addChild(role, node);
|
||||
},
|
||||
"deleteNode " + mut.nodeId
|
||||
});
|
||||
return "";
|
||||
}
|
||||
|
||||
// --- rollback ---
|
||||
|
||||
static void rollback(std::vector<UndoAction>& undoStack) {
|
||||
for (auto it = undoStack.rbegin(); it != undoStack.rend(); ++it) {
|
||||
it->undo();
|
||||
}
|
||||
undoStack.clear();
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
ASTNode* findById(ASTNode* node, const std::string& id) const {
|
||||
if (!node) return nullptr;
|
||||
if (node->id == id) return node;
|
||||
for (auto* child : node->allChildren()) {
|
||||
auto* found = findById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Find which role a child occupies in its parent
|
||||
static std::string findRole(const ASTNode* parent, const ASTNode* child) {
|
||||
for (const auto& role : parent->childRoles()) {
|
||||
for (const auto* kid : parent->getChildren(role)) {
|
||||
if (kid == child) return role;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static std::string getStringProperty(const ASTNode* node,
|
||||
const std::string& prop) {
|
||||
const auto& ct = node->conceptType;
|
||||
if (prop == "name") {
|
||||
if (ct == "Function") return static_cast<const Function*>(node)->name;
|
||||
if (ct == "Variable") return static_cast<const Variable*>(node)->name;
|
||||
if (ct == "Parameter") return static_cast<const Parameter*>(node)->name;
|
||||
if (ct == "Module") return static_cast<const Module*>(node)->name;
|
||||
}
|
||||
if (prop == "op" && ct == "BinaryOperation")
|
||||
return static_cast<const BinaryOperation*>(node)->op;
|
||||
if (prop == "variableName" && ct == "VariableReference")
|
||||
return static_cast<const VariableReference*>(node)->variableName;
|
||||
if (prop == "targetLanguage" && ct == "Module")
|
||||
return static_cast<const Module*>(node)->targetLanguage;
|
||||
return "";
|
||||
}
|
||||
|
||||
bool setStringProperty(ASTNode* node, const std::string& prop,
|
||||
const std::string& value) const {
|
||||
const auto& ct = node->conceptType;
|
||||
if (prop == "name") {
|
||||
if (ct == "Function") { static_cast<Function*>(node)->name = value; return true; }
|
||||
if (ct == "Variable") { static_cast<Variable*>(node)->name = value; return true; }
|
||||
if (ct == "Parameter") { static_cast<Parameter*>(node)->name = value; return true; }
|
||||
if (ct == "Module") { static_cast<Module*>(node)->name = value; return true; }
|
||||
}
|
||||
if (prop == "op" && ct == "BinaryOperation") {
|
||||
static_cast<BinaryOperation*>(node)->op = value; return true;
|
||||
}
|
||||
if (prop == "variableName" && ct == "VariableReference") {
|
||||
static_cast<VariableReference*>(node)->variableName = value; return true;
|
||||
}
|
||||
if (prop == "targetLanguage" && ct == "Module") {
|
||||
static_cast<Module*>(node)->targetLanguage = value; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ASTNode* root_ = nullptr;
|
||||
std::vector<std::string> journal_;
|
||||
};
|
||||
261
editor/src/ContextAPI.h
Normal file
261
editor/src/ContextAPI.h
Normal file
@@ -0,0 +1,261 @@
|
||||
#pragma once
|
||||
// Step 62: Context API
|
||||
//
|
||||
// ContextAPI: provides scope, call-hierarchy, and data-flow queries
|
||||
// over the SemAnno AST.
|
||||
//
|
||||
// Methods:
|
||||
// getInScopeSymbols — variables/functions/parameters visible at a node
|
||||
// getCallHierarchy — callers and callees of a function
|
||||
// getDependencyGraph — data-flow dependencies of a node (via VariableRef)
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Module.h"
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
#include "ast/Parameter.h"
|
||||
#include "ast/Expression.h"
|
||||
|
||||
class ContextAPI {
|
||||
public:
|
||||
struct Symbol {
|
||||
std::string name;
|
||||
std::string kind; // "function", "variable", "parameter"
|
||||
std::string nodeId;
|
||||
};
|
||||
|
||||
struct CallInfo {
|
||||
std::string functionId;
|
||||
std::string functionName;
|
||||
std::vector<std::string> callerIds; // functions that call this one
|
||||
std::vector<std::string> calleeIds; // functions called by this one
|
||||
};
|
||||
|
||||
void setRoot(ASTNode* root) { root_ = root; }
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// getInScopeSymbols — walk up from nodeId, collecting symbols
|
||||
// ---------------------------------------------------------------
|
||||
std::vector<Symbol> getInScopeSymbols(const std::string& nodeId) const {
|
||||
ASTNode* node = findById(root_, nodeId);
|
||||
if (!node) return {};
|
||||
|
||||
std::vector<Symbol> symbols;
|
||||
|
||||
// Walk up the ancestor chain (including node itself)
|
||||
const ASTNode* cur = node;
|
||||
while (cur) {
|
||||
if (cur->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(cur);
|
||||
// Parameters
|
||||
for (auto* p : fn->getChildren("parameters")) {
|
||||
if (p->conceptType == "Parameter") {
|
||||
auto* param = static_cast<const Parameter*>(p);
|
||||
addSymbolUnique(symbols, {param->name, "parameter", param->id});
|
||||
}
|
||||
}
|
||||
// Body variables
|
||||
for (auto* child : fn->getChildren("body")) {
|
||||
if (child->conceptType == "Variable") {
|
||||
auto* v = static_cast<const Variable*>(child);
|
||||
addSymbolUnique(symbols, {v->name, "variable", v->id});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cur->conceptType == "Module") {
|
||||
auto* mod = static_cast<const Module*>(cur);
|
||||
// Module-level variables
|
||||
for (auto* child : mod->getChildren("variables")) {
|
||||
if (child->conceptType == "Variable") {
|
||||
auto* v = static_cast<const Variable*>(child);
|
||||
addSymbolUnique(symbols, {v->name, "variable", v->id});
|
||||
}
|
||||
}
|
||||
// Module-level functions
|
||||
for (auto* child : mod->getChildren("functions")) {
|
||||
if (child->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(child);
|
||||
addSymbolUnique(symbols, {fn->name, "function", fn->id});
|
||||
}
|
||||
}
|
||||
}
|
||||
cur = cur->parent;
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// getCallHierarchy
|
||||
// ---------------------------------------------------------------
|
||||
CallInfo getCallHierarchy(const std::string& functionId) const {
|
||||
CallInfo info;
|
||||
ASTNode* fnNode = findById(root_, functionId);
|
||||
if (!fnNode || fnNode->conceptType != "Function") return info;
|
||||
|
||||
auto* fn = static_cast<Function*>(fnNode);
|
||||
info.functionId = fn->id;
|
||||
info.functionName = fn->name;
|
||||
|
||||
// Collect ALL FunctionCall nodes in the entire tree
|
||||
std::vector<ASTNode*> allCalls;
|
||||
collectByType(root_, "FunctionCall", allCalls);
|
||||
|
||||
// callerIds: functions that contain a call to fn->name
|
||||
// calleeIds: functions called from within fn
|
||||
for (auto* callNode : allCalls) {
|
||||
auto* fc = static_cast<FunctionCall*>(callNode);
|
||||
|
||||
if (fc->functionName == fn->name) {
|
||||
// Someone calls our function — find the enclosing Function
|
||||
Function* caller = enclosingFunction(callNode);
|
||||
if (caller && caller->id != fn->id) {
|
||||
addUnique(info.callerIds, caller->id);
|
||||
}
|
||||
}
|
||||
|
||||
// Is this call inside our function?
|
||||
if (isDescendantOf(callNode, fnNode)) {
|
||||
// Resolve callee function ID by name
|
||||
std::string calleeId = resolveFunctionId(fc->functionName);
|
||||
if (!calleeId.empty()) {
|
||||
addUnique(info.calleeIds, calleeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// getDependencyGraph — data-flow deps via VariableReference
|
||||
// ---------------------------------------------------------------
|
||||
std::vector<std::string> getDependencyGraph(const std::string& nodeId) const {
|
||||
ASTNode* node = findById(root_, nodeId);
|
||||
if (!node) return {};
|
||||
|
||||
// Collect all VariableReference nodes in the subtree
|
||||
std::vector<ASTNode*> refs;
|
||||
collectByType(node, "VariableReference", refs);
|
||||
|
||||
std::vector<std::string> deps;
|
||||
for (auto* refNode : refs) {
|
||||
auto* vr = static_cast<VariableReference*>(refNode);
|
||||
// Resolve to the declaration node (Variable or Parameter)
|
||||
std::string declId = resolveSymbolId(vr->variableName, refNode);
|
||||
if (!declId.empty()) {
|
||||
addUnique(deps, declId);
|
||||
}
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
|
||||
private:
|
||||
// --- helpers ---
|
||||
|
||||
ASTNode* findById(ASTNode* node, const std::string& id) const {
|
||||
if (!node) return nullptr;
|
||||
if (node->id == id) return node;
|
||||
for (auto* child : node->allChildren()) {
|
||||
auto* found = findById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void collectByType(ASTNode* node, const std::string& type,
|
||||
std::vector<ASTNode*>& out) {
|
||||
if (!node) return;
|
||||
if (node->conceptType == type) out.push_back(node);
|
||||
for (auto* child : node->allChildren()) {
|
||||
collectByType(child, type, out);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk up to find the nearest enclosing Function
|
||||
static Function* enclosingFunction(ASTNode* node) {
|
||||
ASTNode* cur = node->parent;
|
||||
while (cur) {
|
||||
if (cur->conceptType == "Function")
|
||||
return static_cast<Function*>(cur);
|
||||
cur = cur->parent;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if child is a descendant of ancestor
|
||||
static bool isDescendantOf(const ASTNode* child, const ASTNode* ancestor) {
|
||||
const ASTNode* cur = child->parent;
|
||||
while (cur) {
|
||||
if (cur == ancestor) return true;
|
||||
cur = cur->parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve a function name to its node ID (searching module-level functions)
|
||||
std::string resolveFunctionId(const std::string& name) const {
|
||||
if (!root_) return "";
|
||||
// Find the module (root or first module child)
|
||||
const ASTNode* mod = root_;
|
||||
if (mod->conceptType != "Module") return "";
|
||||
|
||||
for (auto* child : mod->getChildren("functions")) {
|
||||
if (child->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(child);
|
||||
if (fn->name == name) return fn->id;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Resolve a variable/parameter name to its declaration ID,
|
||||
// searching up the scope chain from refNode.
|
||||
std::string resolveSymbolId(const std::string& name,
|
||||
const ASTNode* refNode) const {
|
||||
const ASTNode* cur = refNode;
|
||||
while (cur) {
|
||||
if (cur->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(cur);
|
||||
for (auto* p : fn->getChildren("parameters")) {
|
||||
if (p->conceptType == "Parameter" &&
|
||||
static_cast<const Parameter*>(p)->name == name)
|
||||
return p->id;
|
||||
}
|
||||
for (auto* child : fn->getChildren("body")) {
|
||||
if (child->conceptType == "Variable" &&
|
||||
static_cast<const Variable*>(child)->name == name)
|
||||
return child->id;
|
||||
}
|
||||
}
|
||||
else if (cur->conceptType == "Module") {
|
||||
for (auto* child : cur->getChildren("variables")) {
|
||||
if (child->conceptType == "Variable" &&
|
||||
static_cast<const Variable*>(child)->name == name)
|
||||
return child->id;
|
||||
}
|
||||
}
|
||||
cur = cur->parent;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static void addSymbolUnique(std::vector<Symbol>& symbols,
|
||||
const Symbol& sym) {
|
||||
for (const auto& s : symbols) {
|
||||
if (s.nodeId == sym.nodeId) return;
|
||||
}
|
||||
symbols.push_back(sym);
|
||||
}
|
||||
|
||||
static void addUnique(std::vector<std::string>& vec,
|
||||
const std::string& val) {
|
||||
if (std::find(vec.begin(), vec.end(), val) == vec.end())
|
||||
vec.push_back(val);
|
||||
}
|
||||
|
||||
ASTNode* root_ = nullptr;
|
||||
};
|
||||
359
editor/src/WebSocketServer.h
Normal file
359
editor/src/WebSocketServer.h
Normal file
@@ -0,0 +1,359 @@
|
||||
#pragma once
|
||||
// Step 59: WebSocket agent endpoint
|
||||
//
|
||||
// WebSocketAgentServer: manages agent sessions and routes JSON-RPC messages.
|
||||
// Agents connect via WebSocket and exchange JSON-RPC over the connection.
|
||||
//
|
||||
// Architecture:
|
||||
// WebSocketTransport (abstract) — handles raw WebSocket I/O
|
||||
// WebSocketAgentServer — session management + JSON-RPC dispatch
|
||||
// MockWebSocketTransport — for testing without real sockets
|
||||
//
|
||||
// The server runs alongside the existing stdin/stdout JSON-RPC in
|
||||
// orchestrator_main.cpp. Both share the same processRequest() handler
|
||||
// via the setRequestHandler() callback.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AgentSession — represents one connected agent
|
||||
// ---------------------------------------------------------------------------
|
||||
struct AgentSession {
|
||||
std::string sessionId;
|
||||
std::string agentName; // optional, set via setAgentName RPC
|
||||
bool connected = false;
|
||||
uint64_t connectedAtMs = 0;
|
||||
uint64_t lastMessageAtMs = 0;
|
||||
int messageCount = 0;
|
||||
};
|
||||
|
||||
// Callback types
|
||||
using JsonRpcHandler = std::function<json(const json& request)>;
|
||||
using SessionEventCallback =
|
||||
std::function<void(const AgentSession& session, const std::string& event)>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WebSocketTransport — abstract transport interface
|
||||
// ---------------------------------------------------------------------------
|
||||
class WebSocketTransport {
|
||||
public:
|
||||
virtual ~WebSocketTransport() = default;
|
||||
|
||||
virtual bool start(int port) = 0;
|
||||
virtual void stop() = 0;
|
||||
virtual bool isRunning() const = 0;
|
||||
|
||||
// Send a text message to a connected session
|
||||
virtual bool sendMessage(const std::string& sessionId,
|
||||
const std::string& message) = 0;
|
||||
|
||||
// --- handler registration (called by WebSocketAgentServer) ---
|
||||
|
||||
void setMessageHandler(
|
||||
std::function<void(const std::string& sessionId,
|
||||
const std::string& message)> handler) {
|
||||
messageHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void setConnectHandler(
|
||||
std::function<void(const std::string& sessionId)> handler) {
|
||||
connectHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void setDisconnectHandler(
|
||||
std::function<void(const std::string& sessionId)> handler) {
|
||||
disconnectHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
protected:
|
||||
std::function<void(const std::string&, const std::string&)> messageHandler_;
|
||||
std::function<void(const std::string&)> connectHandler_;
|
||||
std::function<void(const std::string&)> disconnectHandler_;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WebSocketAgentServer
|
||||
// ---------------------------------------------------------------------------
|
||||
class WebSocketAgentServer {
|
||||
public:
|
||||
explicit WebSocketAgentServer(std::unique_ptr<WebSocketTransport> transport)
|
||||
: transport_(std::move(transport))
|
||||
{
|
||||
transport_->setMessageHandler(
|
||||
[this](const std::string& sid, const std::string& msg) {
|
||||
onMessage(sid, msg);
|
||||
});
|
||||
transport_->setConnectHandler(
|
||||
[this](const std::string& sid) { onConnect(sid); });
|
||||
transport_->setDisconnectHandler(
|
||||
[this](const std::string& sid) { onDisconnect(sid); });
|
||||
}
|
||||
|
||||
bool start(int port) {
|
||||
port_ = port;
|
||||
return transport_->start(port);
|
||||
}
|
||||
|
||||
void stop() { transport_->stop(); }
|
||||
|
||||
bool isRunning() const { return transport_->isRunning(); }
|
||||
int getPort() const { return port_; }
|
||||
|
||||
// Register a fallback JSON-RPC handler for methods the server doesn't
|
||||
// handle itself (e.g. the orchestrator's processRequest).
|
||||
void setRequestHandler(JsonRpcHandler handler) {
|
||||
requestHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
// Register a callback for session lifecycle events ("connected" /
|
||||
// "disconnected").
|
||||
void setSessionEventCallback(SessionEventCallback cb) {
|
||||
sessionEventCallback_ = std::move(cb);
|
||||
}
|
||||
|
||||
// --- session queries ---
|
||||
|
||||
const AgentSession* getSession(const std::string& sessionId) const {
|
||||
auto it = sessions_.find(sessionId);
|
||||
return it != sessions_.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
std::vector<AgentSession> getActiveSessions() const {
|
||||
std::vector<AgentSession> result;
|
||||
for (const auto& [id, s] : sessions_) {
|
||||
if (s.connected) result.push_back(s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t getActiveSessionCount() const {
|
||||
size_t n = 0;
|
||||
for (const auto& [id, s] : sessions_) {
|
||||
if (s.connected) ++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private:
|
||||
// --- transport callbacks ---
|
||||
|
||||
void onConnect(const std::string& sessionId) {
|
||||
AgentSession s;
|
||||
s.sessionId = sessionId;
|
||||
s.connected = true;
|
||||
s.connectedAtMs = nowMs();
|
||||
s.lastMessageAtMs = s.connectedAtMs;
|
||||
sessions_[sessionId] = s;
|
||||
|
||||
if (sessionEventCallback_)
|
||||
sessionEventCallback_(s, "connected");
|
||||
}
|
||||
|
||||
void onDisconnect(const std::string& sessionId) {
|
||||
auto it = sessions_.find(sessionId);
|
||||
if (it != sessions_.end()) {
|
||||
it->second.connected = false;
|
||||
if (sessionEventCallback_)
|
||||
sessionEventCallback_(it->second, "disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
void onMessage(const std::string& sessionId, const std::string& message) {
|
||||
// Update session stats
|
||||
auto it = sessions_.find(sessionId);
|
||||
if (it != sessions_.end()) {
|
||||
it->second.lastMessageAtMs = nowMs();
|
||||
it->second.messageCount++;
|
||||
}
|
||||
|
||||
// Parse + dispatch
|
||||
json response;
|
||||
try {
|
||||
json request = json::parse(message);
|
||||
response = processRequest(request, sessionId);
|
||||
} catch (const std::exception& e) {
|
||||
response = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", nullptr},
|
||||
{"error", {{"code", -32700},
|
||||
{"message", std::string("Parse error: ") + e.what()}}}
|
||||
};
|
||||
}
|
||||
|
||||
transport_->sendMessage(sessionId, response.dump());
|
||||
}
|
||||
|
||||
// Route a JSON-RPC request. Built-in methods are handled first;
|
||||
// everything else is forwarded to the registered handler.
|
||||
json processRequest(const json& request, const std::string& sessionId) {
|
||||
json response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response["id"] = request.contains("id") ? request["id"] : json(nullptr);
|
||||
|
||||
try {
|
||||
std::string method = request.at("method").get<std::string>();
|
||||
|
||||
if (method == "ping") {
|
||||
response["result"] = json{
|
||||
{"message", "pong"},
|
||||
{"sessionId", sessionId}
|
||||
};
|
||||
}
|
||||
else if (method == "getSessionInfo") {
|
||||
auto* sess = getSession(sessionId);
|
||||
if (sess) {
|
||||
response["result"] = {
|
||||
{"sessionId", sess->sessionId},
|
||||
{"agentName", sess->agentName},
|
||||
{"connected", sess->connected},
|
||||
{"messageCount", sess->messageCount}
|
||||
};
|
||||
} else {
|
||||
response["error"] = {
|
||||
{"code", -32603},
|
||||
{"message", "Session not found"}
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (method == "setAgentName") {
|
||||
std::string name =
|
||||
request.at("params").at("name").get<std::string>();
|
||||
auto sit = sessions_.find(sessionId);
|
||||
if (sit != sessions_.end()) {
|
||||
sit->second.agentName = name;
|
||||
response["result"] = true;
|
||||
} else {
|
||||
response["error"] = {
|
||||
{"code", -32603},
|
||||
{"message", "Session not found"}
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (method == "listSessions") {
|
||||
json arr = json::array();
|
||||
for (const auto& [id, s] : sessions_) {
|
||||
if (s.connected) {
|
||||
arr.push_back({
|
||||
{"sessionId", s.sessionId},
|
||||
{"agentName", s.agentName},
|
||||
{"messageCount", s.messageCount}
|
||||
});
|
||||
}
|
||||
}
|
||||
response["result"] = arr;
|
||||
}
|
||||
else if (requestHandler_) {
|
||||
// Delegate to the orchestrator's processRequest
|
||||
response = requestHandler_(request);
|
||||
// Ensure jsonrpc and id are set
|
||||
response["jsonrpc"] = "2.0";
|
||||
if (!response.contains("id"))
|
||||
response["id"] = request.contains("id")
|
||||
? request["id"] : json(nullptr);
|
||||
}
|
||||
else {
|
||||
response["error"] = {
|
||||
{"code", -32601},
|
||||
{"message", "Method not found: " + method}
|
||||
};
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
response["error"] = {
|
||||
{"code", -32600},
|
||||
{"message", std::string("Invalid request: ") + e.what()}
|
||||
};
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static uint64_t nowMs() {
|
||||
using namespace std::chrono;
|
||||
return static_cast<uint64_t>(
|
||||
duration_cast<milliseconds>(
|
||||
steady_clock::now().time_since_epoch())
|
||||
.count());
|
||||
}
|
||||
|
||||
std::unique_ptr<WebSocketTransport> transport_;
|
||||
std::map<std::string, AgentSession> sessions_;
|
||||
JsonRpcHandler requestHandler_;
|
||||
SessionEventCallback sessionEventCallback_;
|
||||
int port_ = 0;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MockWebSocketTransport — for testing without real sockets
|
||||
// ---------------------------------------------------------------------------
|
||||
class MockWebSocketTransport : public WebSocketTransport {
|
||||
public:
|
||||
bool start(int port) override {
|
||||
port_ = port;
|
||||
running_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void stop() override { running_ = false; }
|
||||
|
||||
bool isRunning() const override { return running_; }
|
||||
|
||||
bool sendMessage(const std::string& sessionId,
|
||||
const std::string& message) override {
|
||||
sentMessages_[sessionId].push_back(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- test helpers: simulate agent behaviour ---
|
||||
|
||||
// Simulate an agent opening a WebSocket connection
|
||||
std::string simulateConnect() {
|
||||
std::string sessionId = "agent_" + std::to_string(nextId_++);
|
||||
if (connectHandler_) connectHandler_(sessionId);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
// Simulate an agent closing its connection
|
||||
void simulateDisconnect(const std::string& sessionId) {
|
||||
if (disconnectHandler_) disconnectHandler_(sessionId);
|
||||
}
|
||||
|
||||
// Simulate an agent sending a text message
|
||||
void simulateMessage(const std::string& sessionId,
|
||||
const std::string& message) {
|
||||
if (messageHandler_) messageHandler_(sessionId, message);
|
||||
}
|
||||
|
||||
// Get all messages the server sent back to a session
|
||||
std::vector<std::string> getSentMessages(
|
||||
const std::string& sessionId) const {
|
||||
auto it = sentMessages_.find(sessionId);
|
||||
return it != sentMessages_.end()
|
||||
? it->second
|
||||
: std::vector<std::string>{};
|
||||
}
|
||||
|
||||
// Get the most recent message sent to a session
|
||||
std::string getLastSentMessage(const std::string& sessionId) const {
|
||||
auto it = sentMessages_.find(sessionId);
|
||||
if (it != sentMessages_.end() && !it->second.empty())
|
||||
return it->second.back();
|
||||
return "";
|
||||
}
|
||||
|
||||
// Clear recorded messages (useful between test cases)
|
||||
void clearSentMessages() { sentMessages_.clear(); }
|
||||
|
||||
private:
|
||||
bool running_ = false;
|
||||
int port_ = 0;
|
||||
int nextId_ = 1;
|
||||
std::map<std::string, std::vector<std::string>> sentMessages_;
|
||||
};
|
||||
@@ -63,6 +63,19 @@ public:
|
||||
return result;
|
||||
}
|
||||
|
||||
// Remove a child from whatever role it belongs to
|
||||
bool removeChild(ASTNode* child) {
|
||||
for (auto& [role, kids] : children_) {
|
||||
auto it = std::find(kids.begin(), kids.end(), child);
|
||||
if (it != kids.end()) {
|
||||
(*it)->parent = nullptr;
|
||||
kids.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, std::vector<ASTNode*>> children_;
|
||||
};
|
||||
|
||||
300
editor/tests/step59_test.cpp
Normal file
300
editor/tests/step59_test.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
// Step 59 TDD Test: WebSocket agent endpoint
|
||||
//
|
||||
// Tests the WebSocketAgentServer with MockWebSocketTransport:
|
||||
// 1. Server starts and reports running
|
||||
// 2. Agent connects and receives a session ID
|
||||
// 3. Agent sends JSON-RPC `ping`, receives `pong` with session ID
|
||||
// 4. Multiple agents get unique session IDs
|
||||
// 5. Agent disconnect is tracked (session marked inactive)
|
||||
// 6. Session event callbacks fire on connect/disconnect
|
||||
// 7. Invalid JSON produces a parse-error response
|
||||
// 8. Unknown method produces method-not-found error
|
||||
// 9. Custom request handler delegation works
|
||||
// 10. listSessions returns all active agents
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "WebSocketServer.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
static bool contains(const std::string& haystack, const std::string& needle) {
|
||||
return haystack.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
// --- Test 1: Server starts and reports running ---
|
||||
{
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
auto* tp = transport.get(); // keep raw ptr for test helpers
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
|
||||
assert(!server.isRunning() && "Server should not be running before start");
|
||||
|
||||
bool ok = server.start(9090);
|
||||
assert(ok && "Server should start successfully");
|
||||
assert(server.isRunning() && "Server should be running after start");
|
||||
assert(server.getPort() == 9090 && "Port should match");
|
||||
|
||||
server.stop();
|
||||
assert(!server.isRunning() && "Server should stop");
|
||||
|
||||
std::cout << "Test 1 PASS: Server starts and stops correctly" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 2: Agent connects and gets session ID ---
|
||||
{
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
auto* tp = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
server.start(9090);
|
||||
|
||||
std::string sid = tp->simulateConnect();
|
||||
assert(!sid.empty() && "Session ID should not be empty");
|
||||
|
||||
const AgentSession* session = server.getSession(sid);
|
||||
assert(session != nullptr && "Session should exist after connect");
|
||||
assert(session->connected && "Session should be marked connected");
|
||||
assert(session->sessionId == sid && "Session ID should match");
|
||||
assert(server.getActiveSessionCount() == 1 && "Should have 1 active session");
|
||||
|
||||
std::cout << "Test 2 PASS: Agent connects and gets session ID" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 3: Agent sends ping, receives pong with session ID ---
|
||||
{
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
auto* tp = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
server.start(9090);
|
||||
|
||||
std::string sid = tp->simulateConnect();
|
||||
|
||||
json pingRequest = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", 1},
|
||||
{"method", "ping"}
|
||||
};
|
||||
tp->simulateMessage(sid, pingRequest.dump());
|
||||
|
||||
std::string response = tp->getLastSentMessage(sid);
|
||||
assert(!response.empty() && "Should receive a response");
|
||||
|
||||
json resp = json::parse(response);
|
||||
assert(resp.contains("result") && "Response should have result");
|
||||
assert(resp["result"]["message"] == "pong" && "Result message should be pong");
|
||||
assert(resp["result"]["sessionId"] == sid && "Result should include session ID");
|
||||
assert(resp["id"] == 1 && "Response ID should match request");
|
||||
|
||||
std::cout << "Test 3 PASS: ping returns pong with session ID" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 4: Multiple agents get unique session IDs ---
|
||||
{
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
auto* tp = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
server.start(9090);
|
||||
|
||||
std::string sid1 = tp->simulateConnect();
|
||||
std::string sid2 = tp->simulateConnect();
|
||||
std::string sid3 = tp->simulateConnect();
|
||||
|
||||
assert(sid1 != sid2 && "Session IDs should be unique");
|
||||
assert(sid2 != sid3 && "Session IDs should be unique");
|
||||
assert(sid1 != sid3 && "Session IDs should be unique");
|
||||
assert(server.getActiveSessionCount() == 3 && "Should have 3 active sessions");
|
||||
|
||||
std::cout << "Test 4 PASS: Multiple agents get unique session IDs" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 5: Agent disconnect is tracked ---
|
||||
{
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
auto* tp = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
server.start(9090);
|
||||
|
||||
std::string sid = tp->simulateConnect();
|
||||
assert(server.getActiveSessionCount() == 1);
|
||||
|
||||
tp->simulateDisconnect(sid);
|
||||
|
||||
const AgentSession* session = server.getSession(sid);
|
||||
assert(session != nullptr && "Session should still exist");
|
||||
assert(!session->connected && "Session should be marked disconnected");
|
||||
assert(server.getActiveSessionCount() == 0 && "No active sessions after disconnect");
|
||||
|
||||
std::cout << "Test 5 PASS: Agent disconnect tracked correctly" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 6: Session event callbacks fire ---
|
||||
{
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
auto* tp = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
|
||||
std::vector<std::string> events;
|
||||
server.setSessionEventCallback(
|
||||
[&events](const AgentSession& s, const std::string& event) {
|
||||
events.push_back(s.sessionId + ":" + event);
|
||||
});
|
||||
|
||||
server.start(9090);
|
||||
|
||||
std::string sid = tp->simulateConnect();
|
||||
assert(events.size() == 1 && "Should have 1 event after connect");
|
||||
assert(contains(events[0], "connected") && "Event should be 'connected'");
|
||||
|
||||
tp->simulateDisconnect(sid);
|
||||
assert(events.size() == 2 && "Should have 2 events after disconnect");
|
||||
assert(contains(events[1], "disconnected") && "Event should be 'disconnected'");
|
||||
|
||||
std::cout << "Test 6 PASS: Session event callbacks fire correctly" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 7: Invalid JSON produces parse-error response ---
|
||||
{
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
auto* tp = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
server.start(9090);
|
||||
|
||||
std::string sid = tp->simulateConnect();
|
||||
tp->simulateMessage(sid, "this is not json!!!");
|
||||
|
||||
std::string response = tp->getLastSentMessage(sid);
|
||||
json resp = json::parse(response);
|
||||
assert(resp.contains("error") && "Should have error for invalid JSON");
|
||||
assert(resp["error"]["code"] == -32700 && "Error code should be -32700 (Parse error)");
|
||||
|
||||
std::cout << "Test 7 PASS: Invalid JSON returns parse error" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 8: Unknown method produces method-not-found error ---
|
||||
{
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
auto* tp = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
server.start(9090);
|
||||
|
||||
std::string sid = tp->simulateConnect();
|
||||
json req = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", 42},
|
||||
{"method", "nonExistentMethod"}
|
||||
};
|
||||
tp->simulateMessage(sid, req.dump());
|
||||
|
||||
std::string response = tp->getLastSentMessage(sid);
|
||||
json resp = json::parse(response);
|
||||
assert(resp.contains("error") && "Should have error for unknown method");
|
||||
assert(resp["error"]["code"] == -32601 && "Error code should be -32601 (Method not found)");
|
||||
assert(resp["id"] == 42 && "Response ID should match request");
|
||||
|
||||
std::cout << "Test 8 PASS: Unknown method returns method-not-found error" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 9: Custom request handler delegation ---
|
||||
{
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
auto* tp = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
|
||||
// Register a custom handler that knows about "getAST"
|
||||
server.setRequestHandler([](const json& request) -> json {
|
||||
std::string method = request.at("method").get<std::string>();
|
||||
json response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response["id"] = request.contains("id") ? request["id"] : json(nullptr);
|
||||
if (method == "getAST") {
|
||||
response["result"] = {{"concept", "Module"}, {"name", "TestModule"}};
|
||||
} else {
|
||||
response["error"] = {{"code", -32601}, {"message", "Method not found"}};
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
server.start(9090);
|
||||
std::string sid = tp->simulateConnect();
|
||||
|
||||
json req = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", 10},
|
||||
{"method", "getAST"}
|
||||
};
|
||||
tp->simulateMessage(sid, req.dump());
|
||||
|
||||
std::string response = tp->getLastSentMessage(sid);
|
||||
json resp = json::parse(response);
|
||||
assert(resp.contains("result") && "Delegated handler should return result");
|
||||
assert(resp["result"]["concept"] == "Module" && "Should get Module from handler");
|
||||
assert(resp["id"] == 10 && "Response ID should match");
|
||||
|
||||
std::cout << "Test 9 PASS: Custom request handler delegation works" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 10: listSessions returns all active agents ---
|
||||
{
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
auto* tp = transport.get();
|
||||
WebSocketAgentServer server(std::move(transport));
|
||||
server.start(9090);
|
||||
|
||||
std::string sid1 = tp->simulateConnect();
|
||||
std::string sid2 = tp->simulateConnect();
|
||||
|
||||
// Set agent name on sid1
|
||||
json nameReq = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", 1},
|
||||
{"method", "setAgentName"},
|
||||
{"params", {{"name", "CodeAssistant"}}}
|
||||
};
|
||||
tp->simulateMessage(sid1, nameReq.dump());
|
||||
|
||||
// Disconnect sid2
|
||||
tp->simulateDisconnect(sid2);
|
||||
|
||||
// Ask for session list from sid1
|
||||
json listReq = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", 2},
|
||||
{"method", "listSessions"}
|
||||
};
|
||||
tp->simulateMessage(sid1, listReq.dump());
|
||||
|
||||
std::string response = tp->getLastSentMessage(sid1);
|
||||
json resp = json::parse(response);
|
||||
assert(resp.contains("result") && "Should have result");
|
||||
assert(resp["result"].is_array() && "Result should be an array");
|
||||
// Only sid1 should be active (sid2 disconnected)
|
||||
assert(resp["result"].size() == 1 && "Should have 1 active session");
|
||||
assert(resp["result"][0]["agentName"] == "CodeAssistant" &&
|
||||
"Agent name should be set");
|
||||
|
||||
std::cout << "Test 10 PASS: listSessions returns active agents" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Summary ---
|
||||
std::cout << "\n=== Step 59 Results: " << passed << " passed, "
|
||||
<< failed << " failed ===" << std::endl;
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
// Step 61 TDD Test: AST mutation API (extend)
|
||||
//
|
||||
// Tests extended mutation API with validation and lock checking:
|
||||
// 1. updateNode bulk property update
|
||||
// 2. Mutation recorded in operation journal
|
||||
// 3. Mutation on OptimizationLock node produces warning (not rejection)
|
||||
// 4. deleteNode removes a node from its parent
|
||||
// 5. insertNode adds a node at the correct position
|
||||
//
|
||||
// Will fail until the extended mutation API is implemented.
|
||||
// 1. setProperty updates a node property
|
||||
// 2. updateNode bulk property update
|
||||
// 3. Mutation recorded in operation journal
|
||||
// 4. Mutation on OptimizationLock node produces warning (not rejection)
|
||||
// 5. deleteNode removes a node from its parent
|
||||
// 6. insertNode adds a node at the correct position
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
@@ -20,42 +19,7 @@
|
||||
#include "ast/Statement.h"
|
||||
#include "ast/Expression.h"
|
||||
#include "ast/Annotation.h"
|
||||
|
||||
// Forward declaration — ASTMutationAPI
|
||||
class ASTMutationAPI {
|
||||
public:
|
||||
struct MutationResult {
|
||||
bool success;
|
||||
std::string warning; // Non-empty if a warning was produced
|
||||
std::string error; // Non-empty if mutation failed
|
||||
};
|
||||
|
||||
// Set the root AST to mutate
|
||||
void setRoot(ASTNode* root);
|
||||
|
||||
// Update multiple properties on a node at once
|
||||
MutationResult updateNode(const std::string& nodeId,
|
||||
const std::map<std::string, std::string>& properties);
|
||||
|
||||
// Delete a node by ID (removes from parent)
|
||||
MutationResult deleteNode(const std::string& nodeId);
|
||||
|
||||
// Insert a node as child of parentId in the given role
|
||||
MutationResult insertNode(const std::string& parentId,
|
||||
const std::string& role,
|
||||
ASTNode* node);
|
||||
|
||||
// Set a property on a node
|
||||
MutationResult setProperty(const std::string& nodeId,
|
||||
const std::string& property,
|
||||
const std::string& value);
|
||||
|
||||
// Get operation journal entries since last call
|
||||
std::vector<std::string> getJournalEntries() const;
|
||||
|
||||
// Clear journal
|
||||
void clearJournal();
|
||||
};
|
||||
#include "ASTMutationAPI.h"
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// 2. getCallHierarchy returns callers and callees
|
||||
// 3. getDependencyGraph returns data flow dependencies
|
||||
// 4. Scope respects nesting (inner scope sees outer, not vice versa)
|
||||
//
|
||||
// Will fail until the Context API is implemented.
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
@@ -20,35 +18,7 @@
|
||||
#include "ast/Parameter.h"
|
||||
#include "ast/Statement.h"
|
||||
#include "ast/Expression.h"
|
||||
|
||||
// Forward declaration — ContextAPI
|
||||
class ContextAPI {
|
||||
public:
|
||||
struct Symbol {
|
||||
std::string name;
|
||||
std::string kind; // "function", "variable", "parameter"
|
||||
std::string nodeId;
|
||||
};
|
||||
|
||||
struct CallInfo {
|
||||
std::string functionId;
|
||||
std::string functionName;
|
||||
std::vector<std::string> callerIds; // Functions that call this one
|
||||
std::vector<std::string> calleeIds; // Functions called by this one
|
||||
};
|
||||
|
||||
// Set the root AST
|
||||
void setRoot(ASTNode* root);
|
||||
|
||||
// Get symbols in scope at a given node position
|
||||
std::vector<Symbol> getInScopeSymbols(const std::string& nodeId) const;
|
||||
|
||||
// Get call hierarchy for a function
|
||||
CallInfo getCallHierarchy(const std::string& functionId) const;
|
||||
|
||||
// Get data dependency node IDs for a given node
|
||||
std::vector<std::string> getDependencyGraph(const std::string& nodeId) const;
|
||||
};
|
||||
#include "ContextAPI.h"
|
||||
|
||||
static bool hasSymbol(const std::vector<ContextAPI::Symbol>& symbols, const std::string& name) {
|
||||
return std::any_of(symbols.begin(), symbols.end(),
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
// 1. applySequence of mutations applies all atomically
|
||||
// 2. If one mutation in the batch fails, all preceding mutations roll back
|
||||
// 3. Successful batch records all operations in journal
|
||||
// 4. Partial failure doesn't leave AST in inconsistent state
|
||||
//
|
||||
// Will fail until applySequence with rollback is implemented.
|
||||
// 4. Empty batch succeeds trivially
|
||||
// 5. Partial failure doesn't leave AST in inconsistent state
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
@@ -19,38 +18,7 @@
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
#include "ast/Expression.h"
|
||||
|
||||
// Forward declaration — BatchMutationAPI
|
||||
class BatchMutationAPI {
|
||||
public:
|
||||
struct Mutation {
|
||||
std::string type; // "setProperty", "insertNode", "deleteNode"
|
||||
std::string nodeId;
|
||||
std::string property; // For setProperty
|
||||
std::string value; // For setProperty
|
||||
std::string parentId; // For insertNode
|
||||
std::string role; // For insertNode
|
||||
ASTNode* newNode = nullptr; // For insertNode
|
||||
};
|
||||
|
||||
struct BatchResult {
|
||||
bool success;
|
||||
int appliedCount; // How many mutations were applied before failure
|
||||
std::string error; // Error message if failed
|
||||
};
|
||||
|
||||
// Set the root AST
|
||||
void setRoot(ASTNode* root);
|
||||
|
||||
// Apply a sequence of mutations atomically (all-or-nothing)
|
||||
BatchResult applySequence(const std::vector<Mutation>& mutations);
|
||||
|
||||
// Get journal entries
|
||||
std::vector<std::string> getJournalEntries() const;
|
||||
|
||||
// Clear journal
|
||||
void clearJournal();
|
||||
};
|
||||
#include "BatchMutationAPI.h"
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
|
||||
Reference in New Issue
Block a user