Step 389: add configurable routing rules engine
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2380,4 +2380,13 @@ target_link_libraries(step388_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step389_test tests/step389_test.cpp)
|
||||
target_include_directories(step389_test PRIVATE src)
|
||||
target_link_libraries(step389_test PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
unofficial::tree-sitter::tree-sitter
|
||||
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
351
editor/src/RoutingRules.h
Normal file
351
editor/src/RoutingRules.h
Normal file
@@ -0,0 +1,351 @@
|
||||
#pragma once
|
||||
// Step 389: RoutingRules — Configurable, composable routing rules engine
|
||||
//
|
||||
// Makes routing rules configurable rather than hardcoded in RoutingEngine.
|
||||
// Rules have conditions (AND-combined) and actions. First matching rule wins.
|
||||
// Default rules encode the same logic as RoutingEngine's hardcoded cascade.
|
||||
|
||||
#include "WorkItem.h"
|
||||
#include "RoutingEngine.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
|
||||
// --- RuleCondition ---
|
||||
|
||||
struct RuleCondition {
|
||||
std::string type; // "annotation" | "complexity" | "contextWidth" | "nodeType"
|
||||
// | "rejectionCount" | "language" | "pattern"
|
||||
std::string property; // for annotation: annotation type/property name
|
||||
std::string op; // "==" | "!=" | "<" | ">" | "<=" | ">="
|
||||
std::string value; // comparison target (string or numeric-as-string)
|
||||
|
||||
json toJson() const {
|
||||
return json{{"type", type}, {"property", property}, {"op", op}, {"value", value}};
|
||||
}
|
||||
|
||||
static RuleCondition fromJson(const json& j) {
|
||||
RuleCondition c;
|
||||
if (j.contains("type")) c.type = j["type"].get<std::string>();
|
||||
if (j.contains("property")) c.property = j["property"].get<std::string>();
|
||||
if (j.contains("op")) c.op = j["op"].get<std::string>();
|
||||
if (j.contains("value")) c.value = j["value"].get<std::string>();
|
||||
return c;
|
||||
}
|
||||
};
|
||||
|
||||
// --- RoutingAction ---
|
||||
|
||||
struct RoutingAction {
|
||||
std::string workerType; // "deterministic"|"template"|"slm"|"llm"|"human"
|
||||
bool reviewRequired = false;
|
||||
std::string contextWidthOverride; // optional: override context width
|
||||
float budgetMultiplier = 1.0f; // multiply token budget
|
||||
|
||||
json toJson() const {
|
||||
json j = {{"workerType", workerType}, {"reviewRequired", reviewRequired},
|
||||
{"budgetMultiplier", budgetMultiplier}};
|
||||
if (!contextWidthOverride.empty()) j["contextWidthOverride"] = contextWidthOverride;
|
||||
return j;
|
||||
}
|
||||
|
||||
static RoutingAction fromJson(const json& j) {
|
||||
RoutingAction a;
|
||||
if (j.contains("workerType")) a.workerType = j["workerType"].get<std::string>();
|
||||
if (j.contains("reviewRequired")) a.reviewRequired = j["reviewRequired"].get<bool>();
|
||||
if (j.contains("contextWidthOverride")) a.contextWidthOverride = j["contextWidthOverride"].get<std::string>();
|
||||
if (j.contains("budgetMultiplier")) a.budgetMultiplier = j["budgetMultiplier"].get<float>();
|
||||
return a;
|
||||
}
|
||||
};
|
||||
|
||||
// --- RoutingRule ---
|
||||
|
||||
struct RoutingRule {
|
||||
std::string name;
|
||||
int priority = 100; // lower = higher priority
|
||||
std::vector<RuleCondition> conditions; // all must match (AND)
|
||||
RoutingAction action;
|
||||
|
||||
json toJson() const {
|
||||
json condArr = json::array();
|
||||
for (const auto& c : conditions) condArr.push_back(c.toJson());
|
||||
return json{{"name", name}, {"priority", priority},
|
||||
{"conditions", condArr}, {"action", action.toJson()}};
|
||||
}
|
||||
|
||||
static RoutingRule fromJson(const json& j) {
|
||||
RoutingRule r;
|
||||
if (j.contains("name")) r.name = j["name"].get<std::string>();
|
||||
if (j.contains("priority")) r.priority = j["priority"].get<int>();
|
||||
if (j.contains("conditions")) {
|
||||
for (const auto& c : j["conditions"]) r.conditions.push_back(RuleCondition::fromJson(c));
|
||||
}
|
||||
if (j.contains("action")) r.action = RoutingAction::fromJson(j["action"]);
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Condition evaluation helpers ---
|
||||
|
||||
namespace routing_rules_detail {
|
||||
|
||||
inline bool compareNumeric(const std::string& op, float lhs, float rhs) {
|
||||
if (op == "==" || op == "=") return lhs == rhs;
|
||||
if (op == "!=") return lhs != rhs;
|
||||
if (op == "<") return lhs < rhs;
|
||||
if (op == ">") return lhs > rhs;
|
||||
if (op == "<=") return lhs <= rhs;
|
||||
if (op == ">=") return lhs >= rhs;
|
||||
return false;
|
||||
}
|
||||
|
||||
inline float safeFloat(const std::string& s) {
|
||||
try { return std::stof(s); } catch (...) { return 0.0f; }
|
||||
}
|
||||
|
||||
inline bool evaluateCondition(const RuleCondition& cond, const WorkItem& item) {
|
||||
if (cond.type == "annotation") {
|
||||
// Check workerType annotation (the most common case)
|
||||
if (cond.property == "workerType" || cond.property == "Automatability") {
|
||||
if (cond.op == "==" || cond.op == "=") return item.workerType == cond.value;
|
||||
if (cond.op == "!=") return item.workerType != cond.value;
|
||||
}
|
||||
if (cond.property == "reviewRequired") {
|
||||
bool val = (cond.value == "true");
|
||||
if (cond.op == "==" || cond.op == "=") return item.reviewRequired == val;
|
||||
if (cond.op == "!=") return item.reviewRequired != val;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cond.type == "complexity") {
|
||||
// Complexity conditions compare against priority as a proxy
|
||||
// (priority maps: critical=0, high=1, medium=2, low=3)
|
||||
float itemVal = static_cast<float>(priorityToInt(item.priority));
|
||||
float threshold = safeFloat(cond.value);
|
||||
return compareNumeric(cond.op, itemVal, threshold);
|
||||
}
|
||||
|
||||
if (cond.type == "contextWidth") {
|
||||
std::string width = item.contextWidth.empty() ? "local" : item.contextWidth;
|
||||
if (cond.op == "==" || cond.op == "=") return width == cond.value;
|
||||
if (cond.op == "!=") return width != cond.value;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cond.type == "nodeType") {
|
||||
if (cond.op == "==" || cond.op == "=") return item.nodeType == cond.value;
|
||||
if (cond.op == "!=") return item.nodeType != cond.value;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cond.type == "rejectionCount") {
|
||||
float count = static_cast<float>(item.rejectionHistory.size());
|
||||
float threshold = safeFloat(cond.value);
|
||||
return compareNumeric(cond.op, count, threshold);
|
||||
}
|
||||
|
||||
if (cond.type == "language") {
|
||||
// Language is not directly on WorkItem, use bufferId extension as hint
|
||||
std::string ext;
|
||||
auto dot = item.bufferId.rfind('.');
|
||||
if (dot != std::string::npos) ext = item.bufferId.substr(dot + 1);
|
||||
if (cond.op == "==" || cond.op == "=") return ext == cond.value || item.bufferId == cond.value;
|
||||
if (cond.op == "!=") return ext != cond.value && item.bufferId != cond.value;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cond.type == "pattern") {
|
||||
bool isGetter = isGetterSetterPattern(item.nodeName);
|
||||
bool isConstructor = (item.nodeName == "__init__" || item.nodeName == "constructor" ||
|
||||
item.nodeName.find("Constructor") != std::string::npos);
|
||||
if (cond.value == "getter-setter") return isGetter;
|
||||
if (cond.value == "constructor") return isConstructor;
|
||||
if (cond.value == "simple") return isGetter || isConstructor;
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace routing_rules_detail
|
||||
|
||||
// --- RulesEngine ---
|
||||
|
||||
class RulesEngine {
|
||||
public:
|
||||
void addRule(const RoutingRule& rule) {
|
||||
rules_.push_back(rule);
|
||||
sortRules();
|
||||
}
|
||||
|
||||
void clearRules() { rules_.clear(); }
|
||||
|
||||
size_t ruleCount() const { return rules_.size(); }
|
||||
|
||||
// Evaluate: first matching rule wins
|
||||
RoutingDecision evaluate(const WorkItem& item) const {
|
||||
for (const auto& rule : rules_) {
|
||||
if (matchesAll(rule, item)) {
|
||||
return applyAction(rule, item);
|
||||
}
|
||||
}
|
||||
// Fall through to default routing
|
||||
return defaultRoute(item);
|
||||
}
|
||||
|
||||
// Get the default rules (encoding the same logic as RoutingEngine's hardcoded cascade)
|
||||
static std::vector<RoutingRule> getDefaultRules() {
|
||||
std::vector<RoutingRule> rules;
|
||||
|
||||
// Rule 1: Explicit worker type override (highest priority)
|
||||
{
|
||||
RoutingRule r;
|
||||
r.name = "explicit-worker";
|
||||
r.priority = 10;
|
||||
r.conditions.push_back({"annotation", "workerType", "!=", ""});
|
||||
r.action.workerType = ""; // Will be filled from item
|
||||
rules.push_back(r);
|
||||
}
|
||||
|
||||
// Rule 2: Getter/setter pattern → template
|
||||
{
|
||||
RoutingRule r;
|
||||
r.name = "getter-setter-template";
|
||||
r.priority = 20;
|
||||
r.conditions.push_back({"pattern", "", "==", "getter-setter"});
|
||||
r.action.workerType = "template";
|
||||
r.action.reviewRequired = false;
|
||||
rules.push_back(r);
|
||||
}
|
||||
|
||||
// Rule 3: Cross-project context → LLM
|
||||
{
|
||||
RoutingRule r;
|
||||
r.name = "cross-project-llm";
|
||||
r.priority = 30;
|
||||
r.conditions.push_back({"contextWidth", "", "==", "cross-project"});
|
||||
r.action.workerType = "llm";
|
||||
r.action.reviewRequired = false;
|
||||
rules.push_back(r);
|
||||
}
|
||||
|
||||
// Rule 4: Project context → LLM
|
||||
{
|
||||
RoutingRule r;
|
||||
r.name = "project-context-llm";
|
||||
r.priority = 40;
|
||||
r.conditions.push_back({"contextWidth", "", "==", "project"});
|
||||
r.action.workerType = "llm";
|
||||
r.action.reviewRequired = false;
|
||||
rules.push_back(r);
|
||||
}
|
||||
|
||||
// Rule 5: Rejection escalation (>=1 rejection → escalate)
|
||||
{
|
||||
RoutingRule r;
|
||||
r.name = "rejection-escalation";
|
||||
r.priority = 50;
|
||||
r.conditions.push_back({"rejectionCount", "", ">=", "1"});
|
||||
r.action.workerType = "llm"; // escalated
|
||||
r.action.reviewRequired = true;
|
||||
r.action.budgetMultiplier = 1.5f;
|
||||
rules.push_back(r);
|
||||
}
|
||||
|
||||
// Rule 6: Local context default → SLM
|
||||
{
|
||||
RoutingRule r;
|
||||
r.name = "local-default-slm";
|
||||
r.priority = 90;
|
||||
r.conditions.push_back({"contextWidth", "", "==", "local"});
|
||||
r.action.workerType = "slm";
|
||||
r.action.reviewRequired = false;
|
||||
rules.push_back(r);
|
||||
}
|
||||
|
||||
// Rule 7: File context default → SLM
|
||||
{
|
||||
RoutingRule r;
|
||||
r.name = "file-default-slm";
|
||||
r.priority = 91;
|
||||
r.conditions.push_back({"contextWidth", "", "==", "file"});
|
||||
r.action.workerType = "slm";
|
||||
r.action.reviewRequired = false;
|
||||
rules.push_back(r);
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
// Load/save rules from JSON
|
||||
json saveRules() const {
|
||||
json arr = json::array();
|
||||
for (const auto& r : rules_) arr.push_back(r.toJson());
|
||||
return arr;
|
||||
}
|
||||
|
||||
void loadRules(const json& j) {
|
||||
rules_.clear();
|
||||
if (j.is_array()) {
|
||||
for (const auto& elem : j) {
|
||||
rules_.push_back(RoutingRule::fromJson(elem));
|
||||
}
|
||||
sortRules();
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<RoutingRule>& getRules() const { return rules_; }
|
||||
|
||||
private:
|
||||
std::vector<RoutingRule> rules_;
|
||||
|
||||
void sortRules() {
|
||||
std::sort(rules_.begin(), rules_.end(),
|
||||
[](const RoutingRule& a, const RoutingRule& b) {
|
||||
return a.priority < b.priority;
|
||||
});
|
||||
}
|
||||
|
||||
static bool matchesAll(const RoutingRule& rule, const WorkItem& item) {
|
||||
if (rule.conditions.empty()) return true;
|
||||
for (const auto& cond : rule.conditions) {
|
||||
if (!routing_rules_detail::evaluateCondition(cond, item)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static RoutingDecision applyAction(const RoutingRule& rule, const WorkItem& item) {
|
||||
RoutingDecision d;
|
||||
// Special case: explicit-worker rule uses item's own worker type
|
||||
if (rule.name == "explicit-worker" && !item.workerType.empty()) {
|
||||
d.workerType = item.workerType;
|
||||
} else {
|
||||
d.workerType = rule.action.workerType;
|
||||
}
|
||||
d.reviewRequired = rule.action.reviewRequired;
|
||||
d.contextWidth = rule.action.contextWidthOverride.empty()
|
||||
? (item.contextWidth.empty() ? "local" : item.contextWidth)
|
||||
: rule.action.contextWidthOverride;
|
||||
d.contextBudgetTokens = static_cast<int>(
|
||||
estimateContextBudget(d.contextWidth) * rule.action.budgetMultiplier);
|
||||
d.confidence = 0.85f;
|
||||
d.reasoning = "Matched rule: " + rule.name;
|
||||
d.agentRole = agentRoleForWorker(d.workerType);
|
||||
return d;
|
||||
}
|
||||
|
||||
static RoutingDecision defaultRoute(const WorkItem& item) {
|
||||
RoutingDecision d;
|
||||
d.contextWidth = item.contextWidth.empty() ? "local" : item.contextWidth;
|
||||
d.contextBudgetTokens = estimateContextBudget(d.contextWidth);
|
||||
d.workerType = "llm";
|
||||
d.reviewRequired = false;
|
||||
d.confidence = 0.5f;
|
||||
d.reasoning = "Default routing to LLM (no rule matched)";
|
||||
d.agentRole = "generator";
|
||||
return d;
|
||||
}
|
||||
};
|
||||
303
editor/tests/step389_test.cpp
Normal file
303
editor/tests/step389_test.cpp
Normal file
@@ -0,0 +1,303 @@
|
||||
// Step 389: Routing Rules Engine (12 tests)
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "RoutingRules.h"
|
||||
|
||||
static WorkItem makeItem(const std::string& id,
|
||||
const std::string& name = "doWork",
|
||||
const std::string& workerType = "",
|
||||
const std::string& contextWidth = "local",
|
||||
const std::string& priority = "medium") {
|
||||
WorkItem item;
|
||||
item.id = id;
|
||||
item.nodeId = id + "_node";
|
||||
item.nodeName = name;
|
||||
item.nodeType = "Function";
|
||||
item.bufferId = "main.py";
|
||||
item.workerType = workerType;
|
||||
item.contextWidth = contextWidth;
|
||||
item.priority = priority;
|
||||
item.status = WI_PENDING;
|
||||
item.createdAt = workItemTimestamp();
|
||||
return item;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
|
||||
// Test 1: First matching rule wins
|
||||
{
|
||||
RulesEngine engine;
|
||||
RoutingRule r1;
|
||||
r1.name = "rule-A";
|
||||
r1.priority = 10;
|
||||
r1.conditions.push_back({"contextWidth", "", "==", "local"});
|
||||
r1.action.workerType = "template";
|
||||
|
||||
RoutingRule r2;
|
||||
r2.name = "rule-B";
|
||||
r2.priority = 20;
|
||||
r2.conditions.push_back({"contextWidth", "", "==", "local"});
|
||||
r2.action.workerType = "llm";
|
||||
|
||||
engine.addRule(r2);
|
||||
engine.addRule(r1); // added second but lower priority number = higher priority
|
||||
|
||||
auto item = makeItem("t1");
|
||||
auto decision = engine.evaluate(item);
|
||||
assert(decision.workerType == "template");
|
||||
assert(decision.reasoning.find("rule-A") != std::string::npos);
|
||||
std::cout << "PASS: test 1 — first matching rule wins\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 2: Conditions AND together
|
||||
{
|
||||
RulesEngine engine;
|
||||
RoutingRule r;
|
||||
r.name = "local-function";
|
||||
r.priority = 10;
|
||||
r.conditions.push_back({"contextWidth", "", "==", "local"});
|
||||
r.conditions.push_back({"nodeType", "", "==", "Function"});
|
||||
r.action.workerType = "template";
|
||||
engine.addRule(r);
|
||||
|
||||
// Matches both conditions
|
||||
auto item1 = makeItem("t1");
|
||||
assert(engine.evaluate(item1).workerType == "template");
|
||||
|
||||
// Fails nodeType condition
|
||||
auto item2 = makeItem("t2");
|
||||
item2.nodeType = "Class";
|
||||
auto dec2 = engine.evaluate(item2);
|
||||
assert(dec2.workerType == "llm"); // falls through to default
|
||||
std::cout << "PASS: test 2 — conditions AND together\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 3: Annotation condition matches
|
||||
{
|
||||
RulesEngine engine;
|
||||
RoutingRule r;
|
||||
r.name = "explicit-human";
|
||||
r.priority = 10;
|
||||
r.conditions.push_back({"annotation", "workerType", "==", "human"});
|
||||
r.action.workerType = "human";
|
||||
r.action.reviewRequired = true;
|
||||
engine.addRule(r);
|
||||
|
||||
auto item = makeItem("t1", "doWork", "human");
|
||||
auto dec = engine.evaluate(item);
|
||||
assert(dec.workerType == "human");
|
||||
assert(dec.reviewRequired == true);
|
||||
std::cout << "PASS: test 3 — annotation condition matches\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 4: Complexity threshold (via priority proxy)
|
||||
{
|
||||
RulesEngine engine;
|
||||
RoutingRule r;
|
||||
r.name = "high-priority-llm";
|
||||
r.priority = 10;
|
||||
// priority critical=0, high=1 → complexity < 2 means critical or high
|
||||
r.conditions.push_back({"complexity", "", "<", "2"});
|
||||
r.action.workerType = "llm";
|
||||
r.action.budgetMultiplier = 2.0f;
|
||||
engine.addRule(r);
|
||||
|
||||
auto highItem = makeItem("t1", "doWork", "", "local", "high");
|
||||
auto dec1 = engine.evaluate(highItem);
|
||||
assert(dec1.workerType == "llm");
|
||||
|
||||
auto lowItem = makeItem("t2", "doWork", "", "local", "low");
|
||||
auto dec2 = engine.evaluate(lowItem);
|
||||
assert(dec2.workerType == "llm"); // falls through to default (also llm)
|
||||
assert(dec2.reasoning.find("Default") != std::string::npos); // but via default
|
||||
std::cout << "PASS: test 4 — complexity threshold\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 5: Pattern matching (getter-setter)
|
||||
{
|
||||
RulesEngine engine;
|
||||
RoutingRule r;
|
||||
r.name = "getter-template";
|
||||
r.priority = 10;
|
||||
r.conditions.push_back({"pattern", "", "==", "getter-setter"});
|
||||
r.action.workerType = "template";
|
||||
engine.addRule(r);
|
||||
|
||||
auto getter = makeItem("t1", "getName");
|
||||
assert(engine.evaluate(getter).workerType == "template");
|
||||
|
||||
auto setter = makeItem("t2", "setValue");
|
||||
assert(engine.evaluate(setter).workerType == "template");
|
||||
|
||||
auto normal = makeItem("t3", "processData");
|
||||
assert(engine.evaluate(normal).workerType != "template");
|
||||
std::cout << "PASS: test 5 — pattern matching\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 6: Default rules sensible
|
||||
{
|
||||
auto defaults = RulesEngine::getDefaultRules();
|
||||
assert(defaults.size() >= 5);
|
||||
|
||||
// Verify known rules exist
|
||||
bool hasGetterRule = false, hasProjectRule = false;
|
||||
for (const auto& r : defaults) {
|
||||
if (r.name == "getter-setter-template") hasGetterRule = true;
|
||||
if (r.name == "project-context-llm") hasProjectRule = true;
|
||||
}
|
||||
assert(hasGetterRule);
|
||||
assert(hasProjectRule);
|
||||
std::cout << "PASS: test 6 — default rules sensible\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 7: Custom rules override defaults
|
||||
{
|
||||
RulesEngine engine;
|
||||
// Load defaults first
|
||||
auto defaults = RulesEngine::getDefaultRules();
|
||||
for (const auto& r : defaults) engine.addRule(r);
|
||||
|
||||
// Add custom rule with highest priority
|
||||
RoutingRule custom;
|
||||
custom.name = "force-deterministic";
|
||||
custom.priority = 1; // highest
|
||||
custom.conditions.push_back({"contextWidth", "", "==", "local"});
|
||||
custom.action.workerType = "deterministic";
|
||||
engine.addRule(custom);
|
||||
|
||||
auto item = makeItem("t1", "doWork", "", "local");
|
||||
auto dec = engine.evaluate(item);
|
||||
assert(dec.workerType == "deterministic");
|
||||
assert(dec.reasoning.find("force-deterministic") != std::string::npos);
|
||||
std::cout << "PASS: test 7 — custom rules override defaults\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 8: No match falls through to default
|
||||
{
|
||||
RulesEngine engine;
|
||||
// Add a rule that won't match
|
||||
RoutingRule r;
|
||||
r.name = "specific";
|
||||
r.priority = 10;
|
||||
r.conditions.push_back({"contextWidth", "", "==", "cross-project"});
|
||||
r.action.workerType = "human";
|
||||
engine.addRule(r);
|
||||
|
||||
auto item = makeItem("t1", "doWork", "", "local");
|
||||
auto dec = engine.evaluate(item);
|
||||
assert(dec.workerType == "llm"); // default
|
||||
assert(dec.reasoning.find("Default") != std::string::npos);
|
||||
std::cout << "PASS: test 8 — no match falls through to default\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 9: Rule priority ordering
|
||||
{
|
||||
RulesEngine engine;
|
||||
RoutingRule low;
|
||||
low.name = "low-pri";
|
||||
low.priority = 100;
|
||||
low.action.workerType = "slm";
|
||||
engine.addRule(low);
|
||||
|
||||
RoutingRule high;
|
||||
high.name = "high-pri";
|
||||
high.priority = 1;
|
||||
high.action.workerType = "template";
|
||||
engine.addRule(high);
|
||||
|
||||
// Both have empty conditions (match anything), high-pri should win
|
||||
auto item = makeItem("t1");
|
||||
auto dec = engine.evaluate(item);
|
||||
assert(dec.workerType == "template");
|
||||
assert(engine.getRules()[0].name == "high-pri");
|
||||
std::cout << "PASS: test 9 — rule priority ordering\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 10: Rules persist to JSON
|
||||
{
|
||||
RulesEngine engine;
|
||||
RoutingRule r;
|
||||
r.name = "test-rule";
|
||||
r.priority = 42;
|
||||
r.conditions.push_back({"pattern", "", "==", "getter-setter"});
|
||||
r.action.workerType = "template";
|
||||
r.action.budgetMultiplier = 1.5f;
|
||||
engine.addRule(r);
|
||||
|
||||
json saved = engine.saveRules();
|
||||
assert(saved.is_array());
|
||||
assert(saved.size() == 1);
|
||||
assert(saved[0]["name"] == "test-rule");
|
||||
assert(saved[0]["priority"] == 42);
|
||||
|
||||
RulesEngine loaded;
|
||||
loaded.loadRules(saved);
|
||||
assert(loaded.ruleCount() == 1);
|
||||
assert(loaded.getRules()[0].name == "test-rule");
|
||||
assert(loaded.getRules()[0].action.budgetMultiplier == 1.5f);
|
||||
std::cout << "PASS: test 10 — rules persist to JSON\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 11: Budget multiplier applied
|
||||
{
|
||||
RulesEngine engine;
|
||||
RoutingRule r;
|
||||
r.name = "big-budget";
|
||||
r.priority = 10;
|
||||
r.conditions.push_back({"contextWidth", "", "==", "local"});
|
||||
r.action.workerType = "llm";
|
||||
r.action.budgetMultiplier = 3.0f;
|
||||
engine.addRule(r);
|
||||
|
||||
auto item = makeItem("t1");
|
||||
auto dec = engine.evaluate(item);
|
||||
assert(dec.contextBudgetTokens == 500 * 3); // local=500 * 3.0
|
||||
std::cout << "PASS: test 11 — budget multiplier applied\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 12: Rejection count escalation
|
||||
{
|
||||
RulesEngine engine;
|
||||
RoutingRule r;
|
||||
r.name = "escalate-on-reject";
|
||||
r.priority = 10;
|
||||
r.conditions.push_back({"rejectionCount", "", ">=", "2"});
|
||||
r.action.workerType = "human";
|
||||
r.action.reviewRequired = true;
|
||||
engine.addRule(r);
|
||||
|
||||
auto item = makeItem("t1");
|
||||
// No rejections — rule doesn't match
|
||||
auto dec1 = engine.evaluate(item);
|
||||
assert(dec1.workerType != "human");
|
||||
|
||||
// Add 2 rejections
|
||||
RejectionAttempt a1; a1.workerType = "slm"; a1.feedback = "bad";
|
||||
RejectionAttempt a2; a2.workerType = "llm"; a2.feedback = "still bad";
|
||||
item.rejectionHistory.push_back(a1);
|
||||
item.rejectionHistory.push_back(a2);
|
||||
|
||||
auto dec2 = engine.evaluate(item);
|
||||
assert(dec2.workerType == "human");
|
||||
assert(dec2.reviewRequired == true);
|
||||
std::cout << "PASS: test 12 — rejection count escalation\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
std::cout << "\nStep 389 result: " << passed << "/12 tests passed\n";
|
||||
return (passed == 12) ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user