205 lines
7.2 KiB
C++
205 lines
7.2 KiB
C++
|
|
// Step 387: Orchestrator event stream (12 tests)
|
||
|
|
|
||
|
|
#include <cassert>
|
||
|
|
#include <iostream>
|
||
|
|
#include <string>
|
||
|
|
#include "EventStream.h"
|
||
|
|
#include "HeadlessEditorState.h"
|
||
|
|
#include "HeadlessAgentRPCHandler.h"
|
||
|
|
#include "MCPServer.h"
|
||
|
|
|
||
|
|
static WorkItem makeItem(const std::string& id,
|
||
|
|
const std::string& workerType = "template",
|
||
|
|
const std::string& contextWidth = "local") {
|
||
|
|
WorkItem item;
|
||
|
|
item.id = id;
|
||
|
|
item.nodeId = id + "_node";
|
||
|
|
item.nodeName = "getValue";
|
||
|
|
item.nodeType = "Function";
|
||
|
|
item.bufferId = "main.py";
|
||
|
|
item.workerType = workerType;
|
||
|
|
item.contextWidth = contextWidth;
|
||
|
|
item.priority = "medium";
|
||
|
|
item.status = WI_PENDING;
|
||
|
|
item.createdAt = workItemTimestamp();
|
||
|
|
return item;
|
||
|
|
}
|
||
|
|
|
||
|
|
static HeadlessEditorState makeState() {
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("main.py", "def a():\n return 1\n", "python");
|
||
|
|
state.setAgentRole("test-session", AgentRole::Generator);
|
||
|
|
state.workflow = WorkflowState("events");
|
||
|
|
return state;
|
||
|
|
}
|
||
|
|
|
||
|
|
static json rpc(HeadlessEditorState& state, const std::string& method,
|
||
|
|
const json& params = json::object()) {
|
||
|
|
json request = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method},
|
||
|
|
{"params", params}};
|
||
|
|
return handleHeadlessAgentRequest(state, request, "test-session");
|
||
|
|
}
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
int passed = 0;
|
||
|
|
|
||
|
|
// Test 1: emit + poll returns emitted event
|
||
|
|
{
|
||
|
|
EventStream stream;
|
||
|
|
stream.emit({"task.executed", "a", json::object(), workItemTimestamp()});
|
||
|
|
auto events = stream.poll(0);
|
||
|
|
assert(events.size() == 1);
|
||
|
|
assert(events[0].event.type == "task.executed");
|
||
|
|
std::cout << "Test 1 PASSED: emit/poll basic behavior\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 2: poll since version returns only new events
|
||
|
|
{
|
||
|
|
EventStream stream;
|
||
|
|
stream.emit({"x", "a", json::object(), workItemTimestamp()}); // v1
|
||
|
|
stream.emit({"y", "b", json::object(), workItemTimestamp()}); // v2
|
||
|
|
auto newer = stream.poll(1);
|
||
|
|
assert(newer.size() == 1);
|
||
|
|
assert(newer[0].event.type == "y");
|
||
|
|
std::cout << "Test 2 PASSED: poll filters by version\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 3: stream version increments with emits
|
||
|
|
{
|
||
|
|
EventStream stream;
|
||
|
|
assert(stream.getVersion() == 0);
|
||
|
|
stream.emit({"a", "", json::object(), workItemTimestamp()});
|
||
|
|
stream.emit({"b", "", json::object(), workItemTimestamp()});
|
||
|
|
assert(stream.getVersion() == 2);
|
||
|
|
std::cout << "Test 3 PASSED: version tracking\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 4: subscribe callback is invoked
|
||
|
|
{
|
||
|
|
EventStream stream;
|
||
|
|
int callbackCount = 0;
|
||
|
|
stream.subscribe([&callbackCount](const StreamEvent&) {
|
||
|
|
callbackCount++;
|
||
|
|
});
|
||
|
|
stream.emit({"a", "", json::object(), workItemTimestamp()});
|
||
|
|
assert(callbackCount == 1);
|
||
|
|
std::cout << "Test 4 PASSED: subscribe callback fires\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 5: orchestration emits normalized task event types
|
||
|
|
{
|
||
|
|
auto state = makeState();
|
||
|
|
state.workflow->queue.enqueue(makeItem("t1", "template"));
|
||
|
|
rpc(state, "orchestrateAdvance");
|
||
|
|
auto resp = rpc(state, "getEventStream", {{"sinceVersion", 0}});
|
||
|
|
assert(resp.contains("result"));
|
||
|
|
bool hasRouted = false, hasExecuted = false, hasCompleted = false;
|
||
|
|
for (const auto& e : resp["result"]["events"]) {
|
||
|
|
std::string t = e.value("type", "");
|
||
|
|
if (t == "task.routed") hasRouted = true;
|
||
|
|
if (t == "task.executed") hasExecuted = true;
|
||
|
|
if (t == "task.completed") hasCompleted = true;
|
||
|
|
}
|
||
|
|
assert(hasRouted && hasExecuted && hasCompleted);
|
||
|
|
std::cout << "Test 5 PASSED: normalized task event types emitted\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 6: getRecent returns requested event count
|
||
|
|
{
|
||
|
|
EventStream stream;
|
||
|
|
stream.emit({"e1", "", json::object(), workItemTimestamp()});
|
||
|
|
stream.emit({"e2", "", json::object(), workItemTimestamp()});
|
||
|
|
stream.emit({"e3", "", json::object(), workItemTimestamp()});
|
||
|
|
auto recent = stream.getRecent(2);
|
||
|
|
assert(recent.size() == 2);
|
||
|
|
assert(recent[0].event.type == "e2");
|
||
|
|
assert(recent[1].event.type == "e3");
|
||
|
|
std::cout << "Test 6 PASSED: getRecent count + ordering\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 7: empty stream poll returns no events
|
||
|
|
{
|
||
|
|
EventStream stream;
|
||
|
|
auto events = stream.poll(0);
|
||
|
|
assert(events.empty());
|
||
|
|
std::cout << "Test 7 PASSED: empty stream poll behavior\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 8: high-frequency polling does not duplicate results
|
||
|
|
{
|
||
|
|
EventStream stream;
|
||
|
|
stream.emit({"a", "", json::object(), workItemTimestamp()});
|
||
|
|
auto first = stream.poll(0);
|
||
|
|
auto second = stream.poll(stream.getVersion());
|
||
|
|
assert(first.size() == 1);
|
||
|
|
assert(second.empty());
|
||
|
|
std::cout << "Test 8 PASSED: no duplicate events after poll version update\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 9: stream JSON serialization includes required fields
|
||
|
|
{
|
||
|
|
EventStream stream;
|
||
|
|
stream.emit({"task.executed", "x", json{{"k", "v"}}, workItemTimestamp()});
|
||
|
|
auto j = EventStream::toJson(stream.poll(0));
|
||
|
|
assert(j.is_array());
|
||
|
|
assert(j[0].contains("version"));
|
||
|
|
assert(j[0].contains("type"));
|
||
|
|
assert(j[0].contains("timestamp"));
|
||
|
|
std::cout << "Test 9 PASSED: stream JSON serialization\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 10: MCP tool registration includes event stream tools
|
||
|
|
{
|
||
|
|
MCPServer server;
|
||
|
|
bool hasStream = false, hasRecent = false;
|
||
|
|
for (const auto& t : server.getTools()) {
|
||
|
|
if (t.name == "whetstone_get_event_stream") hasStream = true;
|
||
|
|
if (t.name == "whetstone_get_recent_events") hasRecent = true;
|
||
|
|
}
|
||
|
|
assert(hasStream && hasRecent);
|
||
|
|
std::cout << "Test 10 PASSED: MCP event stream tools registered\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 11: getEventStream RPC returns only events since version
|
||
|
|
{
|
||
|
|
auto state = makeState();
|
||
|
|
state.workflow->queue.enqueue(makeItem("t1", "template"));
|
||
|
|
rpc(state, "orchestrateAdvance");
|
||
|
|
auto first = rpc(state, "getEventStream", {{"sinceVersion", 0}});
|
||
|
|
int v = first["result"]["version"].get<int>();
|
||
|
|
auto second = rpc(state, "getEventStream", {{"sinceVersion", v}});
|
||
|
|
assert(first["result"]["events"].size() > 0);
|
||
|
|
assert(second["result"]["events"].empty());
|
||
|
|
std::cout << "Test 11 PASSED: getEventStream since-version behavior\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test 12: getRecentEvents RPC honors count parameter
|
||
|
|
{
|
||
|
|
auto state = makeState();
|
||
|
|
state.workflow->queue.enqueue(makeItem("a", "template"));
|
||
|
|
state.workflow->queue.enqueue(makeItem("b", "template"));
|
||
|
|
rpc(state, "orchestrateAdvance");
|
||
|
|
auto recent = rpc(state, "getRecentEvents", {{"count", 2}});
|
||
|
|
assert(recent.contains("result"));
|
||
|
|
assert(recent["result"]["events"].size() <= 2);
|
||
|
|
std::cout << "Test 12 PASSED: getRecentEvents count behavior\n";
|
||
|
|
passed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::cout << "\nResults: " << passed << "/12\n";
|
||
|
|
assert(passed == 12);
|
||
|
|
return 0;
|
||
|
|
}
|