Files
whetstone_DSL/editor/tests/step1922_test.cpp
Bill 4d47e894c1 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>
2026-03-01 20:28:31 -07:00

136 lines
5.4 KiB
C++

// 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;
}