Sprint 278: LSP Proxy Hardening (steps 1918-1922)

- LSPMessageValidator: JSON-RPC 2.0 shape validation (method, id type, params type)
- LSPCapabilityNegotiator: per-source capability tracking; shouldRoute gates on declared caps
- LSPRequestThrottler: per-URI/method debounce; blocks rapid didChange floods
- LSPErrorRecovery: per-source health tracking; stale-cache fallback when crashed
- Sprint278IntegrationSummary: poly-parse (C++ + Haskell) end-to-end integration

26/26 tests passing (5+5+5+5+6). All new headers well under 600 lines.
LoRA recorded: sprint278-2026-03-01 (9 calls, 6437 tokens).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-03-01 20:28:31 -07:00
parent 67c3c73046
commit 4d47e894c1
11 changed files with 754 additions and 0 deletions

View File

@@ -10957,3 +10957,20 @@ target_link_libraries(step1916_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step1917_test tests/step1917_test.cpp) add_executable(step1917_test tests/step1917_test.cpp)
target_include_directories(step1917_test PRIVATE src) target_include_directories(step1917_test PRIVATE src)
target_link_libraries(step1917_test PRIVATE nlohmann_json::nlohmann_json) target_link_libraries(step1917_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step1918_test tests/step1918_test.cpp)
target_include_directories(step1918_test PRIVATE src)
target_link_libraries(step1918_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step1919_test tests/step1919_test.cpp)
target_include_directories(step1919_test PRIVATE src)
add_executable(step1920_test tests/step1920_test.cpp)
target_include_directories(step1920_test PRIVATE src)
add_executable(step1921_test tests/step1921_test.cpp)
target_include_directories(step1921_test PRIVATE src)
add_executable(step1922_test tests/step1922_test.cpp)
target_include_directories(step1922_test PRIVATE src)
target_link_libraries(step1922_test PRIVATE nlohmann_json::nlohmann_json)

View File

@@ -0,0 +1,58 @@
#pragma once
#include <map>
#include <set>
#include <string>
#include <vector>
namespace whetstone {
class LSPCapabilityNegotiator {
public:
void registerSource(const std::string& source,
const std::vector<std::string>& caps) {
caps_[source] = {caps.begin(), caps.end()};
}
bool supports(const std::string& source,
const std::string& capability) const {
auto it = caps_.find(source);
if (it == caps_.end()) return false;
return it->second.count(capability) > 0;
}
// Returns true if the source supports the capability required by this method.
// Unknown methods pass through (return true).
bool shouldRoute(const std::string& source,
const std::string& method) const {
auto it = methodToCap_.find(method);
if (it == methodToCap_.end()) return true; // unknown method: pass through
return supports(source, it->second);
}
std::vector<std::string> capabilitiesFor(const std::string& source) const {
auto it = caps_.find(source);
if (it == caps_.end()) return {};
return {it->second.begin(), it->second.end()};
}
std::vector<std::string> sourcesSupporting(const std::string& cap) const {
std::vector<std::string> result;
for (const auto& [src, capSet] : caps_)
if (capSet.count(cap)) result.push_back(src);
return result;
}
private:
std::map<std::string, std::set<std::string>> caps_;
const std::map<std::string, std::string> methodToCap_ = {
{"textDocument/hover", "hoverProvider"},
{"textDocument/definition", "definitionProvider"},
{"textDocument/references", "referencesProvider"},
{"textDocument/completion", "completionProvider"},
{"textDocument/rename", "renameProvider"},
{"textDocument/formatting", "documentFormattingProvider"},
};
};
} // namespace whetstone

View File

@@ -0,0 +1,64 @@
#pragma once
#include "LSPDiagnosticsAggregator.h"
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
namespace whetstone {
class LSPErrorRecovery {
public:
void markCrashed(const std::string& source) {
crashed_.insert(source);
}
void markRecovered(const std::string& source) {
crashed_.erase(source);
}
// Unknown sources are healthy (optimistic default).
bool isHealthy(const std::string& source) const {
return crashed_.count(source) == 0;
}
std::vector<std::string> healthySources(
const std::vector<std::string>& all) const {
std::vector<std::string> result;
for (const auto& s : all)
if (isHealthy(s)) result.push_back(s);
return result;
}
void cacheDiagnostics(const std::string& source, const std::string& uri,
const std::vector<Diagnostic>& diags) {
cache_[{source, uri}] = diags;
}
// Returns cached diags if source is crashed, else empty.
std::vector<Diagnostic> fallbackDiagnostics(const std::string& source,
const std::string& uri) const {
if (isHealthy(source)) return {};
auto it = cache_.find({source, uri});
if (it == cache_.end()) return {};
return it->second;
}
void clearCache(const std::string& source) {
for (auto it = cache_.begin(); it != cache_.end(); ) {
if (it->first.first == source) it = cache_.erase(it);
else ++it;
}
}
int crashedCount() const {
return static_cast<int>(crashed_.size());
}
private:
std::set<std::string> crashed_;
std::map<std::pair<std::string, std::string>, std::vector<Diagnostic>> cache_;
};
} // namespace whetstone

View File

@@ -0,0 +1,52 @@
#pragma once
#include <nlohmann/json.hpp>
#include <string>
namespace whetstone {
struct ValidationResult {
bool valid = true;
int errorCode = 0;
std::string errorMessage;
};
class LSPMessageValidator {
public:
ValidationResult validate(const nlohmann::json& msg) const {
// Must have "jsonrpc": "2.0"
if (!msg.contains("jsonrpc") || msg["jsonrpc"] != "2.0")
return {false, -32600, "Invalid Request: missing or wrong jsonrpc version"};
// Must have "method" as a string
if (!msg.contains("method"))
return {false, -32600, "Invalid Request: missing method"};
if (!msg["method"].is_string())
return {false, -32600, "Invalid Request: method must be a string"};
// If "id" present: must be string, number, or null
if (msg.contains("id")) {
const auto& id = msg["id"];
if (!id.is_string() && !id.is_number() && !id.is_null())
return {false, -32600, "Invalid Request: id must be string, number, or null"};
}
// If "params" present: must be object or array
if (msg.contains("params")) {
const auto& p = msg["params"];
if (!p.is_object() && !p.is_array())
return {false, -32602, "Invalid params: must be object or array"};
}
return {true, 0, ""};
}
static bool isNotification(const nlohmann::json& msg) {
return !msg.contains("id");
}
static bool isRequest(const nlohmann::json& msg) {
return msg.contains("id");
}
};
} // namespace whetstone

View File

@@ -0,0 +1,45 @@
#pragma once
#include <cstdint>
#include <map>
#include <string>
#include <utility>
namespace whetstone {
class LSPRequestThrottler {
public:
void record(const std::string& uri, const std::string& method,
int64_t timestamp_ms) {
last_[{uri, method}] = timestamp_ms;
}
bool shouldAllow(const std::string& uri, const std::string& method,
int64_t timestamp_ms, int64_t debounce_ms) const {
auto it = last_.find({uri, method});
if (it == last_.end()) return true;
return (timestamp_ms - it->second) >= debounce_ms;
}
void reset(const std::string& uri) {
for (auto it = last_.begin(); it != last_.end(); ) {
if (it->first.first == uri) it = last_.erase(it);
else ++it;
}
}
void resetAll() {
last_.clear();
}
int64_t lastTimestamp(const std::string& uri,
const std::string& method) const {
auto it = last_.find({uri, method});
if (it == last_.end()) return -1;
return it->second;
}
private:
std::map<std::pair<std::string, std::string>, int64_t> last_;
};
} // namespace whetstone

View File

@@ -0,0 +1,13 @@
#pragma once
#include <string>
namespace whetstone {
struct Sprint278IntegrationSummary {
int stepsCompleted = 5;
bool success = true;
std::string sprintName() const { return "Sprint 278: LSP Proxy Hardening"; }
};
} // namespace whetstone

View File

@@ -0,0 +1,97 @@
// Step 1918: LSPMessageValidator
// Validates JSON-RPC 2.0 message shape before routing.
//
// t1: valid request passes
// t2: missing jsonrpc field rejected
// t3: wrong jsonrpc version rejected
// t4: non-string method rejected; missing method rejected
// t5: bad id type rejected; bad params type rejected; notification/request detection
#include "LSPMessageValidator.h"
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace ws = whetstone;
static int p=0,f=0;
#define T(n) { std::cout<<" "<<#n<<"... "; }
#define P() { std::cout<<"PASS\n"; ++p; }
#define F(m) { std::cout<<"FAIL: "<<m<<"\n"; ++f; }
#define C(c,m) if(!(c)){F(m);return;}
void t1(){
T(valid_request_passes);
ws::LSPMessageValidator v;
json msg = {{"jsonrpc","2.0"},{"id",1},{"method","textDocument/definition"},
{"params",{{"textDocument",{{"uri","file:///a.go"}}}}}};
auto r = v.validate(msg);
C(r.valid, "valid");
C(r.errorCode == 0, "no error code");
C(r.errorMessage.empty(),"no error message");
// notification (no id) also valid
json notif = {{"jsonrpc","2.0"},{"method","textDocument/didOpen"},{"params",json::object()}};
C(v.validate(notif).valid, "notification valid");
P();
}
void t2(){
T(missing_jsonrpc_rejected);
ws::LSPMessageValidator v;
json msg = {{"method","textDocument/hover"}};
auto r = v.validate(msg);
C(!r.valid, "invalid");
C(r.errorCode == -32600, "code -32600");
P();
}
void t3(){
T(wrong_jsonrpc_version_rejected);
ws::LSPMessageValidator v;
json msg = {{"jsonrpc","1.0"},{"method","textDocument/hover"}};
auto r = v.validate(msg);
C(!r.valid, "invalid");
C(r.errorCode == -32600, "code -32600");
P();
}
void t4(){
T(non_string_method_and_missing_method_rejected);
ws::LSPMessageValidator v;
json bad1 = {{"jsonrpc","2.0"},{"method",42}};
C(!v.validate(bad1).valid, "number method invalid");
json bad2 = {{"jsonrpc","2.0"},{"id",1}};
C(!v.validate(bad2).valid, "missing method invalid");
C(v.validate(bad2).errorCode == -32600, "code -32600");
P();
}
void t5(){
T(bad_id_bad_params_and_detection);
ws::LSPMessageValidator v;
// id as object: invalid
json badId = {{"jsonrpc","2.0"},{"method","x"},{"id",json::object()}};
C(!v.validate(badId).valid, "object id invalid");
// id as null: valid
json nullId = {{"jsonrpc","2.0"},{"method","x"},{"id",nullptr}};
C(v.validate(nullId).valid, "null id valid");
// params as string: invalid
json badParams = {{"jsonrpc","2.0"},{"method","x"},{"params","oops"}};
C(!v.validate(badParams).valid, "string params invalid");
C(v.validate(badParams).errorCode == -32602, "params error code -32602");
// isNotification / isRequest
json notif = {{"jsonrpc","2.0"},{"method","didOpen"}};
json req = {{"jsonrpc","2.0"},{"method","definition"},{"id",2}};
C( ws::LSPMessageValidator::isNotification(notif), "notif is notification");
C(!ws::LSPMessageValidator::isNotification(req), "req is not notification");
C( ws::LSPMessageValidator::isRequest(req), "req is request");
C(!ws::LSPMessageValidator::isRequest(notif), "notif is not request");
P();
}
int main(){
std::cout << "Step 1918: LSPMessageValidator\n";
t1(); t2(); t3(); t4(); t5();
std::cout << "\n" << p << "/" << (p+f) << " passed\n";
return f > 0 ? 1 : 0;
}

View File

@@ -0,0 +1,92 @@
// Step 1919: LSPCapabilityNegotiator
// Tracks per-source capability sets; gates routing on capability support.
//
// t1: registerSource and supports
// t2: shouldRoute blocks unsupported method; passes unknown methods
// t3: capabilitiesFor returns sorted capability list
// t4: sourcesSupporting finds all sources with a given capability
// t5: unregistered source supports nothing; shouldRoute passes unknown sources
#include "LSPCapabilityNegotiator.h"
#include <iostream>
namespace ws = whetstone;
static int p=0,f=0;
#define T(n) { std::cout<<" "<<#n<<"... "; }
#define P() { std::cout<<"PASS\n"; ++p; }
#define F(m) { std::cout<<"FAIL: "<<m<<"\n"; ++f; }
#define C(c,m) if(!(c)){F(m);return;}
void t1(){
T(registerSource_and_supports);
ws::LSPCapabilityNegotiator n;
n.registerSource("gopls", {"hoverProvider","definitionProvider","referencesProvider"});
C( n.supports("gopls","hoverProvider"), "gopls has hover");
C( n.supports("gopls","definitionProvider"), "gopls has definition");
C(!n.supports("gopls","completionProvider"), "gopls lacks completion");
C(!n.supports("tsserver","hoverProvider"), "unregistered lacks anything");
P();
}
void t2(){
T(shouldRoute_blocks_unsupported_passes_unknown);
ws::LSPCapabilityNegotiator n;
n.registerSource("gopls", {"hoverProvider","definitionProvider"});
n.registerSource("tsserver", {"hoverProvider","completionProvider"});
// gopls supports hover → route
C( n.shouldRoute("gopls", "textDocument/hover"), "gopls routes hover");
// gopls lacks completion → block
C(!n.shouldRoute("gopls", "textDocument/completion"), "gopls blocks completion");
// tsserver supports completion → route
C( n.shouldRoute("tsserver", "textDocument/completion"), "tsserver routes completion");
// unknown method passes through for all sources
C( n.shouldRoute("gopls", "workspace/symbol"), "unknown method passes");
C( n.shouldRoute("tsserver", "workspace/symbol"), "unknown method passes ts");
P();
}
void t3(){
T(capabilitiesFor_returns_list);
ws::LSPCapabilityNegotiator n;
n.registerSource("gopls", {"hoverProvider","definitionProvider","referencesProvider"});
auto caps = n.capabilitiesFor("gopls");
C(caps.size() == 3, "three caps");
C(n.capabilitiesFor("missing").empty(), "missing source empty");
P();
}
void t4(){
T(sourcesSupporting_finds_all);
ws::LSPCapabilityNegotiator n;
n.registerSource("gopls", {"hoverProvider","definitionProvider"});
n.registerSource("tsserver", {"hoverProvider","completionProvider"});
n.registerSource("pylsp", {"hoverProvider","referencesProvider"});
auto hover = n.sourcesSupporting("hoverProvider");
C(hover.size() == 3, "three hover sources");
auto def = n.sourcesSupporting("definitionProvider");
C(def.size() == 1, "one definition source");
C(def[0] == "gopls", "gopls for definition");
C(n.sourcesSupporting("renameProvider").empty(), "none for rename");
P();
}
void t5(){
T(unregistered_source_behavior);
ws::LSPCapabilityNegotiator n;
// unregistered source: supports() returns false
C(!n.supports("unknown","hoverProvider"), "unregistered lacks caps");
// unregistered source: known method requires capability → shouldRoute false
// because supports() returns false for the required cap
C(!n.shouldRoute("unknown","textDocument/hover"), "unregistered blocked on known method");
// unregistered source: unknown method → passes through
C( n.shouldRoute("unknown","$/custom"), "unknown method passes for unregistered");
P();
}
int main(){
std::cout << "Step 1919: LSPCapabilityNegotiator\n";
t1(); t2(); t3(); t4(); t5();
std::cout << "\n" << p << "/" << (p+f) << " passed\n";
return f > 0 ? 1 : 0;
}

View File

@@ -0,0 +1,80 @@
// Step 1920: LSPRequestThrottler
// Debounces per-URI requests to prevent flooding language servers.
//
// t1: no prior record → shouldAllow true
// t2: within debounce window → shouldAllow false
// t3: after debounce window expires → shouldAllow true
// t4: reset clears uri; resetAll clears everything
// t5: lastTimestamp returns -1 for missing, correct value after record
#include "LSPRequestThrottler.h"
#include <iostream>
namespace ws = whetstone;
static int p=0,f=0;
#define T(n) { std::cout<<" "<<#n<<"... "; }
#define P() { std::cout<<"PASS\n"; ++p; }
#define F(m) { std::cout<<"FAIL: "<<m<<"\n"; ++f; }
#define C(c,m) if(!(c)){F(m);return;}
void t1(){
T(no_prior_record_allows);
ws::LSPRequestThrottler t;
C(t.shouldAllow("file:///a.go","textDocument/didChange",1000,500), "no record allows");
P();
}
void t2(){
T(within_debounce_window_blocks);
ws::LSPRequestThrottler t;
t.record("file:///a.go","textDocument/didChange",1000);
// 200ms later, 500ms debounce → block
C(!t.shouldAllow("file:///a.go","textDocument/didChange",1200,500), "within window blocked");
// different method: not throttled
C( t.shouldAllow("file:///a.go","textDocument/hover",1200,500), "different method allowed");
// different uri: not throttled
C( t.shouldAllow("file:///b.go","textDocument/didChange",1200,500), "different uri allowed");
P();
}
void t3(){
T(after_debounce_window_allows);
ws::LSPRequestThrottler t;
t.record("file:///a.go","textDocument/didChange",1000);
// exactly at boundary: 1000+500=1500 → allow (>=)
C( t.shouldAllow("file:///a.go","textDocument/didChange",1500,500), "at boundary allowed");
// past boundary
C( t.shouldAllow("file:///a.go","textDocument/didChange",1600,500), "past boundary allowed");
P();
}
void t4(){
T(reset_and_resetAll);
ws::LSPRequestThrottler t;
t.record("file:///a.go","textDocument/didChange",1000);
t.record("file:///b.go","textDocument/didChange",1000);
t.reset("file:///a.go");
C( t.shouldAllow("file:///a.go","textDocument/didChange",1100,500), "a.go reset → allowed");
C(!t.shouldAllow("file:///b.go","textDocument/didChange",1100,500), "b.go still throttled");
t.resetAll();
C( t.shouldAllow("file:///b.go","textDocument/didChange",1100,500), "b.go allowed after resetAll");
P();
}
void t5(){
T(lastTimestamp);
ws::LSPRequestThrottler t;
C(t.lastTimestamp("file:///a.go","textDocument/didChange") == -1, "no record returns -1");
t.record("file:///a.go","textDocument/didChange",9999);
C(t.lastTimestamp("file:///a.go","textDocument/didChange") == 9999, "correct timestamp");
C(t.lastTimestamp("file:///a.go","textDocument/hover") == -1, "different method -1");
P();
}
int main(){
std::cout << "Step 1920: LSPRequestThrottler\n";
t1(); t2(); t3(); t4(); t5();
std::cout << "\n" << p << "/" << (p+f) << " passed\n";
return f > 0 ? 1 : 0;
}

View File

@@ -0,0 +1,101 @@
// Step 1921: LSPErrorRecovery
// Tracks per-source health; returns stale cached diagnostics when crashed.
//
// t1: unknown sources are healthy by default
// t2: markCrashed/markRecovered toggle health
// t3: healthySources filters crashed sources
// t4: fallbackDiagnostics returns cache only when crashed
// t5: clearCache removes entries; crashedCount
#include "LSPErrorRecovery.h"
#include <iostream>
namespace ws = whetstone;
static int p=0,f=0;
#define T(n) { std::cout<<" "<<#n<<"... "; }
#define P() { std::cout<<"PASS\n"; ++p; }
#define F(m) { std::cout<<"FAIL: "<<m<<"\n"; ++f; }
#define C(c,m) if(!(c)){F(m);return;}
static ws::Diagnostic diag(const std::string& uri, int sev,
const std::string& msg, const std::string& src) {
ws::Diagnostic d; d.uri=uri; d.severity=sev; d.message=msg; d.source=src; return d;
}
void t1(){
T(unknown_sources_healthy);
ws::LSPErrorRecovery r;
C(r.isHealthy("gopls"), "gopls healthy by default");
C(r.isHealthy("haskell-lsp"),"haskell-lsp healthy by default");
C(r.crashedCount() == 0, "none crashed");
P();
}
void t2(){
T(markCrashed_and_markRecovered);
ws::LSPErrorRecovery r;
r.markCrashed("haskell-lsp");
C(!r.isHealthy("haskell-lsp"), "crashed after markCrashed");
C( r.isHealthy("gopls"), "gopls still healthy");
C(r.crashedCount() == 1, "one crashed");
r.markRecovered("haskell-lsp");
C( r.isHealthy("haskell-lsp"), "healthy after markRecovered");
C(r.crashedCount() == 0, "none crashed after recovery");
P();
}
void t3(){
T(healthySources_filters_crashed);
ws::LSPErrorRecovery r;
r.markCrashed("haskell-lsp");
std::vector<std::string> all = {"gopls","haskell-lsp","tsserver"};
auto healthy = r.healthySources(all);
C(healthy.size() == 2, "two healthy");
C(healthy[0] == "gopls", "gopls healthy");
C(healthy[1] == "tsserver", "tsserver healthy");
P();
}
void t4(){
T(fallbackDiagnostics_only_when_crashed);
ws::LSPErrorRecovery r;
auto d = diag("file:///Parse.hs",1,"type error","haskell-lsp");
r.cacheDiagnostics("haskell-lsp","file:///Parse.hs",{d});
// healthy: fallback is empty
C(r.fallbackDiagnostics("haskell-lsp","file:///Parse.hs").empty(),
"healthy: no fallback");
// crash it: fallback returns cache
r.markCrashed("haskell-lsp");
auto fb = r.fallbackDiagnostics("haskell-lsp","file:///Parse.hs");
C(fb.size() == 1, "one fallback diag");
C(fb[0].message == "type error", "correct message");
// different uri: empty
C(r.fallbackDiagnostics("haskell-lsp","file:///Other.hs").empty(),
"missing uri empty");
P();
}
void t5(){
T(clearCache_and_crashedCount);
ws::LSPErrorRecovery r;
r.cacheDiagnostics("haskell-lsp","file:///A.hs",
{diag("file:///A.hs",1,"E","haskell-lsp")});
r.cacheDiagnostics("haskell-lsp","file:///B.hs",
{diag("file:///B.hs",2,"W","haskell-lsp")});
r.markCrashed("haskell-lsp");
C(!r.fallbackDiagnostics("haskell-lsp","file:///A.hs").empty(), "cache present");
r.clearCache("haskell-lsp");
C(r.fallbackDiagnostics("haskell-lsp","file:///A.hs").empty(), "cache cleared");
// crashedCount tracks multiple
r.markCrashed("gopls");
C(r.crashedCount() == 2, "two crashed");
P();
}
int main(){
std::cout << "Step 1921: LSPErrorRecovery\n";
t1(); t2(); t3(); t4(); t5();
std::cout << "\n" << p << "/" << (p+f) << " passed\n";
return f > 0 ? 1 : 0;
}

View File

@@ -0,0 +1,135 @@
// Step 1922: Sprint 278 Integration
// poly-parse project: C++ lexer component + Haskell AST transform.
// All four hardening components work together:
// - Validator rejects malformed messages
// - Negotiator blocks methods haskell-lsp doesn't support
// - Throttler debounces rapid didChange from the C++ lexer
// - ErrorRecovery serves stale diags when haskell-lsp crashes
//
// t1: validator rejects bad message before routing
// t2: negotiator blocks referencesProvider (haskell-lsp doesn't declare it)
// t3: throttler debounces rapid didChange on lexer.cpp
// t4: error recovery serves stale haskell diags after crash
// t5: healthy gopls still routes normally; haskell-lsp in recovery
// t6: Sprint278IntegrationSummary reports complete
#include "LSPMessageValidator.h"
#include "LSPCapabilityNegotiator.h"
#include "LSPRequestThrottler.h"
#include "LSPErrorRecovery.h"
#include "Sprint278IntegrationSummary.h"
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace ws = whetstone;
static int p=0,f=0;
#define T(n) { std::cout<<" "<<#n<<"... "; }
#define P() { std::cout<<"PASS\n"; ++p; }
#define F(m) { std::cout<<"FAIL: "<<m<<"\n"; ++f; }
#define C(c,m) if(!(c)){F(m);return;}
// poly-parse setup:
// gopls → C++ lexer (hoverProvider, definitionProvider, referencesProvider)
// haskell-lsp → Haskell AST transform (hoverProvider, definitionProvider)
void t1(){
T(validator_rejects_malformed_message);
ws::LSPMessageValidator v;
// message missing "jsonrpc"
json bad = {{"method","textDocument/hover"},{"id",1}};
C(!v.validate(bad).valid, "missing jsonrpc rejected");
// valid message passes
json good = {{"jsonrpc","2.0"},{"id",1},{"method","textDocument/hover"}};
C(v.validate(good).valid, "valid message passes");
P();
}
void t2(){
T(negotiator_blocks_unsupported_method);
ws::LSPCapabilityNegotiator n;
n.registerSource("gopls", {"hoverProvider","definitionProvider","referencesProvider"});
n.registerSource("haskell-lsp", {"hoverProvider","definitionProvider"});
// gopls supports references → route
C( n.shouldRoute("gopls", "textDocument/references"), "gopls routes references");
// haskell-lsp lacks referencesProvider → block
C(!n.shouldRoute("haskell-lsp", "textDocument/references"), "haskell-lsp blocked for references");
// both support hover → route
C( n.shouldRoute("gopls", "textDocument/hover"), "gopls routes hover");
C( n.shouldRoute("haskell-lsp", "textDocument/hover"), "haskell-lsp routes hover");
P();
}
void t3(){
T(throttler_debounces_rapid_didChange);
ws::LSPRequestThrottler t;
const std::string uri = "file:///src/lexer.cpp";
const std::string method = "textDocument/didChange";
const int64_t debounce = 300; // ms
// first request: allowed
C( t.shouldAllow(uri, method, 0, debounce), "first allowed");
t.record(uri, method, 0);
// 100ms later: blocked
C(!t.shouldAllow(uri, method, 100, debounce), "100ms blocked");
// 300ms later: allowed
C( t.shouldAllow(uri, method, 300, debounce), "300ms allowed");
t.record(uri, method, 300);
// hover not throttled
C( t.shouldAllow(uri, "textDocument/hover", 350, debounce), "hover not throttled");
P();
}
void t4(){
T(error_recovery_serves_stale_diags_after_crash);
ws::LSPErrorRecovery r;
ws::Diagnostic d;
d.uri="file:///src/AST.hs"; d.severity=1;
d.message="parse error"; d.source="haskell-lsp";
r.cacheDiagnostics("haskell-lsp","file:///src/AST.hs",{d});
// healthy: no fallback
C(r.fallbackDiagnostics("haskell-lsp","file:///src/AST.hs").empty(),
"healthy: no fallback");
// crash
r.markCrashed("haskell-lsp");
auto fb = r.fallbackDiagnostics("haskell-lsp","file:///src/AST.hs");
C(fb.size() == 1, "one stale diag");
C(fb[0].message == "parse error", "correct stale message");
C(r.crashedCount() == 1, "one crashed source");
P();
}
void t5(){
T(healthy_gopls_routes_normally);
ws::LSPCapabilityNegotiator n;
ws::LSPErrorRecovery r;
n.registerSource("gopls", {"hoverProvider","definitionProvider","referencesProvider"});
n.registerSource("haskell-lsp", {"hoverProvider","definitionProvider"});
r.markCrashed("haskell-lsp");
// gopls healthy and has capability → route
C(r.isHealthy("gopls"), "gopls healthy");
C(n.shouldRoute("gopls","textDocument/hover"), "gopls hover routed");
// haskell-lsp crashed but its hover capability is still registered
// (routing decision is separate from health — health gates fallback logic)
C(!r.isHealthy("haskell-lsp"), "haskell-lsp crashed");
auto healthy = r.healthySources({"gopls","haskell-lsp"});
C(healthy.size() == 1, "one healthy source");
C(healthy[0] == "gopls","gopls is the healthy one");
P();
}
void t6(){
T(sprint278_integration_summary_complete);
ws::Sprint278IntegrationSummary s;
C(s.stepsCompleted == 5, "5 steps");
C(s.success, "success");
C(s.sprintName().find("278") != std::string::npos, "278 in name");
P();
}
int main(){
std::cout << "Step 1922: Sprint 278 Integration (poly-parse: C++ + Haskell)\n";
t1(); t2(); t3(); t4(); t5(); t6();
std::cout << "\n" << p << "/" << (p+f) << " passed\n";
return f > 0 ? 1 : 0;
}