Skip to content

Commit a20a454

Browse files
userFRMclaude
andauthored
fix(test): rename dispatch test honestly + add serializer regression tests (#238)
Addresses Codex review findings: 1. Rename dispatch_covers_all_registered_endpoints → registry_metadata_integrity. The test never called the generated dispatcher — it validated registry metadata only. The doc comment now explains that dispatch-registry alignment is guaranteed at build time (shared ParsedEndpoints source), not by this test. 2. Add 3 MCP serializer field-parity regression tests: - serialize_quote_ticks_includes_condition_and_midpoint_fields - serialize_trade_quote_ticks_includes_extended_fields - serialize_greeks_ticks_includes_all_22_greeks Each asserts specific JSON keys exist, catching future field loss. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4e09d86 commit a20a454

2 files changed

Lines changed: 140 additions & 31 deletions

File tree

crates/thetadatadx/src/endpoint.rs

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -480,53 +480,40 @@ mod tests {
480480
);
481481
}
482482

483-
/// Verify every endpoint in the registry is handled by the generated
484-
/// dispatch function (returns `InvalidParams` for missing args, NOT
485-
/// `UnknownEndpoint`). This catches codegen bugs where a registry
486-
/// entry has no corresponding match arm.
487-
#[tokio::test]
488-
async fn dispatch_covers_all_registered_endpoints() {
483+
/// Verify registry metadata integrity: 61 endpoints, no duplicates,
484+
/// all categories present, no empty descriptions or rest_paths.
485+
///
486+
/// Note: dispatch-registry alignment (every registry name has a
487+
/// generated match arm) is guaranteed at build time — both are
488+
/// generated from the same `ParsedEndpoints` vec in
489+
/// `build_support/endpoints.rs`. A name mismatch is structurally
490+
/// impossible without a build failure. This test validates the
491+
/// registry's content, not dispatch coverage.
492+
#[test]
493+
fn registry_metadata_integrity() {
489494
use crate::registry::ENDPOINTS;
490495

491-
// We don't have a live client, so we can't call invoke_endpoint
492-
// directly. Instead, verify that `invoke_generated_endpoint` with
493-
// empty args returns InvalidParams (meaning the name was matched
494-
// and arg extraction started) rather than UnknownEndpoint.
495-
//
496-
// This requires a ThetaDataDx instance — but we only need the
497-
// dispatch to reach the arg-extraction phase, which happens before
498-
// any gRPC call. Unfortunately the generated function signature
499-
// requires &ThetaDataDx, so we test a weaker property: the
500-
// endpoint name set in the registry matches what the generated
501-
// dispatch accepts. Both are generated from the same TOML source,
502-
// so a mismatch indicates a codegen bug.
503-
//
504-
// The build-time uncovered-RPC check (endpoints.rs) provides the
505-
// compile-time guarantee. This test verifies the runtime registry
506-
// is consistent.
507496
let registry_names: std::collections::HashSet<&str> =
508497
ENDPOINTS.iter().map(|e| e.name).collect();
509498
assert_eq!(
510499
registry_names.len(),
511500
ENDPOINTS.len(),
512501
"duplicate names in ENDPOINTS"
513502
);
514-
// Verify expected count (catches silent endpoint loss)
515503
assert_eq!(
516504
ENDPOINTS.len(),
517505
61,
518506
"expected 61 endpoints, got {}",
519507
ENDPOINTS.len()
520508
);
521-
// Verify all categories are represented
522509
let categories: std::collections::HashSet<&str> =
523510
ENDPOINTS.iter().map(|e| e.category).collect();
524-
assert!(categories.contains("stock"));
525-
assert!(categories.contains("option"));
526-
assert!(categories.contains("index"));
527-
assert!(categories.contains("calendar"));
528-
assert!(categories.contains("rate"));
529-
// Verify every endpoint has non-empty description and rest_path
511+
for expected in ["stock", "option", "index", "calendar", "rate"] {
512+
assert!(
513+
categories.contains(expected),
514+
"missing category: {expected}"
515+
);
516+
}
530517
for ep in ENDPOINTS {
531518
assert!(
532519
!ep.description.is_empty(),
@@ -538,6 +525,8 @@ mod tests {
538525
"endpoint {} has empty rest_path",
539526
ep.name
540527
);
528+
// Every endpoint must have at least a name and return type
529+
assert!(!ep.name.is_empty(), "found endpoint with empty name");
541530
}
542531
}
543532
}

tools/mcp/src/main.rs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1152,7 +1152,7 @@ mod tests {
11521152
use std::collections::HashSet;
11531153

11541154
use super::*;
1155-
use tdbe::types::tick::{EodTick, GreeksTick};
1155+
use tdbe::types::tick::{EodTick, GreeksTick, QuoteTick, TradeQuoteTick};
11561156

11571157
fn sample_eod_tick(expiration: i32, strike: f64, right: i32) -> EodTick {
11581158
EodTick {
@@ -1456,4 +1456,124 @@ mod tests {
14561456
"single-contract rows should not emit wildcard-only right metadata"
14571457
);
14581458
}
1459+
1460+
// ── Serializer field-parity regression tests ──────────────────────
1461+
// These catch future field loss by asserting specific keys exist in
1462+
// the serialized JSON output.
1463+
1464+
#[test]
1465+
fn serialize_quote_ticks_includes_condition_and_midpoint_fields() {
1466+
let tick = QuoteTick {
1467+
ms_of_day: 0,
1468+
bid_size: 100,
1469+
bid_exchange: 11,
1470+
bid: 150.0,
1471+
bid_condition: 1,
1472+
ask_size: 200,
1473+
ask_exchange: 12,
1474+
ask: 151.0,
1475+
ask_condition: 2,
1476+
date: 20260410,
1477+
expiration: 0,
1478+
strike: 0.0,
1479+
right: 0,
1480+
midpoint: 150.5,
1481+
};
1482+
let payload = serialize_quote_ticks(&[tick]);
1483+
let row = payload["ticks"].as_array().unwrap().first().unwrap();
1484+
for key in [
1485+
"bid_condition",
1486+
"ask_condition",
1487+
"midpoint",
1488+
"bid_exchange",
1489+
"ask_exchange",
1490+
] {
1491+
assert!(row.get(key).is_some(), "missing key: {key}");
1492+
}
1493+
}
1494+
1495+
#[test]
1496+
fn serialize_trade_quote_ticks_includes_extended_fields() {
1497+
let tick = TradeQuoteTick {
1498+
ms_of_day: 0,
1499+
sequence: 1,
1500+
ext_condition1: 10,
1501+
ext_condition2: 20,
1502+
ext_condition3: 30,
1503+
ext_condition4: 40,
1504+
condition: 1,
1505+
size: 100,
1506+
exchange: 11,
1507+
price: 150.0,
1508+
condition_flags: 0,
1509+
price_flags: 0,
1510+
volume_type: 1,
1511+
records_back: 0,
1512+
quote_ms_of_day: 34_200_000,
1513+
bid_size: 100,
1514+
bid_exchange: 11,
1515+
bid: 149.0,
1516+
bid_condition: 1,
1517+
ask_size: 200,
1518+
ask_exchange: 12,
1519+
ask: 151.0,
1520+
ask_condition: 2,
1521+
date: 20260410,
1522+
expiration: 0,
1523+
strike: 0.0,
1524+
right: 0,
1525+
};
1526+
let payload = serialize_trade_quote_ticks(&[tick]);
1527+
let row = payload["ticks"].as_array().unwrap().first().unwrap();
1528+
for key in [
1529+
"quote_ms_of_day",
1530+
"bid_exchange",
1531+
"ask_exchange",
1532+
"bid_condition",
1533+
"ask_condition",
1534+
"ext_condition1",
1535+
"ext_condition2",
1536+
"ext_condition3",
1537+
"ext_condition4",
1538+
"condition_flags",
1539+
"price_flags",
1540+
"volume_type",
1541+
"records_back",
1542+
] {
1543+
assert!(row.get(key).is_some(), "missing key: {key}");
1544+
}
1545+
}
1546+
1547+
#[test]
1548+
fn serialize_greeks_ticks_includes_all_22_greeks() {
1549+
let tick = sample_greeks_tick(0, 0.0, 0);
1550+
let payload = serialize_greeks_ticks(&[tick]);
1551+
let row = payload["ticks"].as_array().unwrap().first().unwrap();
1552+
for key in [
1553+
"implied_volatility",
1554+
"delta",
1555+
"gamma",
1556+
"theta",
1557+
"vega",
1558+
"rho",
1559+
"iv_error",
1560+
"vanna",
1561+
"charm",
1562+
"vomma",
1563+
"veta",
1564+
"speed",
1565+
"zomma",
1566+
"color",
1567+
"ultima",
1568+
"d1",
1569+
"d2",
1570+
"dual_delta",
1571+
"dual_gamma",
1572+
"epsilon",
1573+
"lambda",
1574+
"vera",
1575+
] {
1576+
assert!(row.get(key).is_some(), "missing Greek: {key}");
1577+
}
1578+
}
14591579
}

0 commit comments

Comments
 (0)