hydrate alert evidence from clickhouse

This commit is contained in:
dirtydishes 2026-05-17 11:02:30 -04:00
parent cd0a1dd9e5
commit c0b5b6dbeb
10 changed files with 701 additions and 62 deletions

View file

@ -1,5 +1,8 @@
import { describe, expect, it } from "bun:test";
import type { ClickHouseClient } from "../src/clickhouse";
import { alertsTableDDL, ALERTS_TABLE, fromAlertRecord, toAlertRecord } from "../src/alerts";
import { fetchAlertContextByTraceId } from "../src/clickhouse";
import { toFlowPacketRecord } from "../src/flow-packets";
const alert = {
source_ts: 10,
@ -19,6 +22,62 @@ const alert = {
evidence_refs: ["flowpacket:1", "print:1"]
};
const packet = {
source_ts: 11,
ingest_ts: 21,
seq: 2,
trace_id: "flowpacket:1",
id: "flowpacket:1",
members: ["print:1"],
features: {
option_contract_id: "SPY-2026-06-19-500-C",
count: 1,
total_size: 50
},
join_quality: {}
};
const print = {
source_ts: 12,
ingest_ts: 22,
seq: 3,
trace_id: "print:1",
ts: 12,
option_contract_id: "SPY-2026-06-19-500-C",
price: 1.45,
size: 50,
exchange: "XTEST",
conditions: [],
nbbo_side: "A",
execution_nbbo_bid: 1.4,
execution_nbbo_ask: 1.5,
execution_nbbo_mid: 1.45,
execution_nbbo_spread: 0.1,
execution_nbbo_age_ms: 14,
execution_nbbo_side: "A",
execution_underlying_spot: 500.25,
execution_underlying_bid: 500.2,
execution_underlying_ask: 500.3,
execution_underlying_mid: 500.25,
execution_underlying_age_ms: 9,
execution_iv: 0.31,
signal_reasons: [],
signal_pass: true
};
const makeClient = (resolver: (query: string) => unknown[]): ClickHouseClient =>
({
exec: async () => {},
insert: async () => {},
ping: async () => ({ success: true }),
close: async () => {},
query: async ({ query }: { query: string }) => ({
async json<T>() {
return resolver(query) as T;
}
})
}) as ClickHouseClient;
describe("alerts storage helpers", () => {
it("includes the correct table name in the DDL", () => {
const ddl = alertsTableDDL();
@ -33,4 +92,51 @@ describe("alerts storage helpers", () => {
expect(restored.evidence_refs).toEqual(alert.evidence_refs);
expect(restored.severity).toBe(alert.severity);
});
it("fetches persisted alert context and reports unresolved refs", async () => {
const contextAlert = {
...alert,
trace_id: "alert:ctx",
evidence_refs: ["flowpacket:1", "print:1", "print:missing"]
};
const queries: string[] = [];
const client = makeClient((query) => {
queries.push(query);
if (query.includes(ALERTS_TABLE)) {
return [toAlertRecord(contextAlert)];
}
if (query.includes("flow_packets")) {
return [toFlowPacketRecord(packet)];
}
if (query.includes("option_prints")) {
return [print];
}
return [];
});
const bundle = await fetchAlertContextByTraceId(client, "alert:ctx");
expect(bundle.alert?.trace_id).toBe("alert:ctx");
expect(bundle.flow_packets.map((item) => item.id)).toEqual(["flowpacket:1"]);
expect(bundle.option_prints.map((item) => item.trace_id)).toEqual(["print:1"]);
expect(bundle.option_prints[0]?.execution_nbbo_side).toBe("A");
expect(bundle.option_prints[0]?.execution_nbbo_bid).toBe(1.4);
expect(bundle.option_prints[0]?.execution_underlying_spot).toBe(500.25);
expect(bundle.option_prints[0]?.execution_iv).toBe(0.31);
expect(bundle.missing_refs).toEqual(["print:missing"]);
expect(queries[0]).toContain("trace_id = 'alert:ctx'");
expect(queries[1]).toContain("id IN");
expect(queries[2]).toContain("trace_id IN ('print:1', 'print:missing')");
});
it("returns an empty context when the alert is missing", async () => {
const bundle = await fetchAlertContextByTraceId(makeClient(() => []), "alert:missing");
expect(bundle).toEqual({
alert: null,
flow_packets: [],
option_prints: [],
missing_refs: []
});
});
});