Files
0pt/tests/analytics/bi-slo.test.ts
T
Claude Opus 5 ed6e62809d
CI (SIAX Cloud) / quality (push) Successful in 54s
CI (SIAX Cloud) / integration (push) Successful in 49s
CI (SIAX Cloud) / security (push) Successful in 28s
Security Scan (SIAX Cloud) / security (push) Successful in 30s
CI (SIAX Cloud) / quality (pull_request) Successful in 58s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m6s
CI (SIAX Cloud) / security (pull_request) Successful in 33s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 39s
feat(analytics): BI-P103 — BI SLOs measured from AUD0's usage events, or declared unmeasured
aud0#34 merged seven BI usage event types and recorded that zero producers
emit them. This makes 0pt the producer and derives the SLOs from those events
rather than opening a second, private measurement path that would disagree
with aud0 during an incident.

Three honest verdicts instead of one number:

  insufficient_data     an availability objective over an empty window is
                        SILENCE, not 100%. Every objective in an empty window
                        comes back insufficient_data and fully_measured is
                        false. A started query with no terminal event counts
                        as unresolved — not as a success.
  not_measurable_here   dashboard FRESHNESS is a property of the dataset
                        (BI-P026/BI-P105 watermark) and report DELIVERY is
                        N0TIFY's receipt. Neither is in aud0's seven types, so
                        both are declared unmeasured rather than proxied by
                        completion.
  low_confidence        a p99 over 12 samples is the 12th-slowest request
                        wearing a percentile's clothes. Reported with its
                        sample size and not judged.

The producer is an allowlist built field by named field, never a spread. The
test feeds it an execution record carrying SQL text, a database error message
quoting a personnummer, filter VALUES, a tenant id and a result sample, then
greps the emitted events — after first proving the grep finds those strings in
the input, so a clean result cannot be a broken detector.

14 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 04:01:16 +02:00

294 lines
11 KiB
TypeScript

/**
* tests/analytics/bi-slo.test.ts
* ===============================
* BI-P103 — BI SLO, and the AUD0 usage events it is measured from.
*
* Two failure modes are attacked directly, because both look like a healthy
* dashboard:
*
* 1. AN SLO REPORT OVER SILENCE. Zero events must not read as 100%
* availability. Every objective in an empty window must come back
* `insufficient_data`, and `fully_measured` must be false.
* 2. A PRODUCER THAT LEAKS. The emitter is handed an execution record
* carrying SQL text, a database error message quoting a personnummer,
* filter VALUES, a tenant id and a result sample — everything AUD0's
* BI-P076 ingest exists to refuse — and none of it may appear in any
* emitted payload. The test greps the serialized events, and first
* proves the grep finds those strings in the INPUT so a clean result
* cannot be a broken detector.
*/
import { describe, expect, it } from 'vitest';
import {
BI_SLO_CATALOGUE,
evaluateBiSlos,
percentile,
summariseSloReport,
type SloResult,
} from '@/src/analytics/slo/bi-slo';
import {
biUsageEventsForExecution,
EMITTABLE_PAYLOAD_KEYS,
type Aud0BiEvent,
type ExecutionTelemetry,
} from '@/src/analytics/slo/usage-events';
const FROM = new Date('2026-09-07T00:00:00.000Z');
const TO = new Date('2026-09-07T23:59:59.000Z');
function queryEvents(args: {
n: number;
surface: 'dashboard' | 'explore';
outcome: 'completed' | 'failed' | 'running';
durationMs: (i: number) => number;
cacheHit?: boolean;
idPrefix?: string;
}): Aud0BiEvent[] {
const out: Aud0BiEvent[] = [];
for (let i = 0; i < args.n; i += 1) {
const t: ExecutionTelemetry = {
query_id: `${args.idPrefix ?? args.surface}_${i}`,
surface: args.surface,
state: args.outcome === 'completed' ? 'succeeded' : args.outcome === 'failed' ? 'failed' : 'running',
started_at: new Date(FROM.getTime() + i * 1000).toISOString(),
finished_at: new Date(FROM.getTime() + i * 1000 + args.durationMs(i)).toISOString(),
duration_ms: args.durationMs(i),
...(args.cacheHit !== undefined ? { cache_hit: args.cacheHit } : {}),
...(args.outcome === 'failed' ? { error_code: 'TIMEOUT' } : {}),
};
out.push(...biUsageEventsForExecution(t));
}
return out;
}
function byId(results: SloResult[], id: string): SloResult {
const r = results.find((x) => x.objective_id === id);
if (!r) throw new Error(`no result for ${id}`);
return r;
}
describe('BI-P103 — an SLO over silence is not a met SLO', () => {
it('an empty window makes every measurable objective insufficient_data, never 100%', () => {
const report = evaluateBiSlos({ events: [], from: FROM, to: TO });
expect(report.fully_measured).toBe(false);
for (const r of report.results) {
expect(['insufficient_data', 'not_measurable_here']).toContain(r.status);
expect(r.status).not.toBe('met');
}
const summary = summariseSloReport(report);
expect(summary.met).toBe(0);
expect(summary.measured_fraction).toBe(0);
});
it('a started query that never finished is unresolved, not a success', () => {
const events = queryEvents({ n: 200, surface: 'dashboard', outcome: 'running', durationMs: () => 100 });
const report = evaluateBiSlos({ events, from: FROM, to: TO });
expect(report.unresolved_queries).toBe(200);
expect(byId(report.results, 'dash.availability').status).toBe('insufficient_data');
expect(byId(report.results, 'dash.availability').observed).toBeUndefined();
});
it('a percentile below min_samples is reported WITH its sample size and not judged', () => {
const events = queryEvents({
n: 12,
surface: 'explore',
outcome: 'completed',
durationMs: (i) => 1000 + i * 10,
});
const p99 = byId(evaluateBiSlos({ events, from: FROM, to: TO }).results, 'explore.p99');
expect(p99.status).toBe('insufficient_data');
expect(p99.low_confidence).toBe(true);
expect(p99.sample_size).toBe(12);
expect(p99.observed).toBeDefined(); // reported…
expect(p99.detail).toMatch(/not a percentile/); // …with the caveat attached
});
it('freshness and delivery are declared not_measurable_here rather than proxied', () => {
const report = evaluateBiSlos({ events: [], from: FROM, to: TO });
const freshness = byId(report.results, 'dash.freshness');
const delivery = byId(report.results, 'report.delivery_sla');
expect(freshness.status).toBe('not_measurable_here');
expect(freshness.detail).toMatch(/analytics_freshness_watermark/);
expect(delivery.status).toBe('not_measurable_here');
expect(delivery.detail).toMatch(/n0tify_delivery_receipt/);
});
});
describe('BI-P103 — real measurement from real events', () => {
it('computes availability, error rate and both dashboard latencies', () => {
const events = [
...queryEvents({
n: 180,
surface: 'dashboard',
outcome: 'completed',
cacheHit: false,
durationMs: (i) => 500 + i * 10,
idPrefix: 'cold',
}),
...queryEvents({
n: 120,
surface: 'dashboard',
outcome: 'completed',
cacheHit: true,
durationMs: () => 120,
idPrefix: 'warm',
}),
...queryEvents({
n: 3,
surface: 'dashboard',
outcome: 'failed',
durationMs: () => 900,
idPrefix: 'bad',
}),
];
const { results } = evaluateBiSlos({ events, from: FROM, to: TO });
const availability = byId(results, 'dash.availability');
expect(availability.sample_size).toBe(303);
expect(availability.observed).toBeCloseTo(300 / 303, 6);
expect(availability.status).toBe('breached'); // 0.9901 < 0.995 target
const errorRate = byId(results, 'dash.error_rate');
expect(errorRate.observed).toBeCloseTo(3 / 303, 6);
const cold = byId(results, 'dash.p95_first_query');
expect(cold.sample_size).toBe(183); // 180 cold + 3 failures, which had no cache hit
expect(cold.status).toBe('met');
const cached = byId(results, 'dash.p95_cached');
expect(cached.sample_size).toBe(120);
expect(cached.observed).toBe(120);
expect(cached.status).toBe('met');
});
it('a breach is reported as a breach', () => {
const events = queryEvents({
n: 60,
surface: 'dashboard',
outcome: 'completed',
cacheHit: false,
durationMs: () => 9000,
});
expect(
byId(evaluateBiSlos({ events, from: FROM, to: TO }).results, 'dash.p95_first_query'),
).toMatchObject({ status: 'breached', observed: 9000 });
});
it('events outside the window do not count', () => {
const events = queryEvents({ n: 200, surface: 'dashboard', outcome: 'completed', durationMs: () => 100 });
const report = evaluateBiSlos({
events,
from: new Date('2026-09-08T00:00:00.000Z'),
to: new Date('2026-09-08T23:59:59.000Z'),
});
expect(byId(report.results, 'dash.availability').status).toBe('insufficient_data');
});
it('percentile is nearest-rank — it never invents a value nobody experienced', () => {
const sample = [10, 20, 30, 40];
expect(percentile(sample, 0.95)).toBe(40);
expect(percentile(sample, 0.5)).toBe(20);
expect(percentile([], 0.95)).toBeUndefined();
});
it('the catalogue covers all three workload classes and every field BI-P103 names', () => {
const kinds = new Set(BI_SLO_CATALOGUE.map((o) => `${o.workload}:${o.kind}`));
for (const expected of [
'interactive_dashboard:availability',
'interactive_dashboard:latency_p95_first',
'interactive_dashboard:latency_p95_cached',
'interactive_dashboard:error_rate',
'interactive_dashboard:freshness',
'exploration:latency_p95',
'exploration:latency_p99',
'exploration:max_result_rows',
'exploration:timeout',
'report:completion_sla',
'report:delivery_sla',
]) {
expect(kinds.has(expected), expected).toBe(true);
}
});
});
describe('BI-P103 — the producer cannot leak a result into AUD0', () => {
const hostile: ExecutionTelemetry = {
query_id: 'q_1',
compiled_query_hash: 'f'.repeat(64),
compiled_sql: "SELECT * FROM customers WHERE personnummer = '19800101-1234'",
error_message: 'duplicate key value violates unique constraint (personnummer)=(19800101-1234)',
error_code: 'CONFLICT',
tenant_id: 't_alpha',
dataset_id: 'ds_sales',
engine: 'clickhouse',
surface: 'dashboard',
state: 'failed',
started_at: '2026-09-07T10:00:00.000Z',
finished_at: '2026-09-07T10:00:02.000Z',
duration_ms: 2000,
filter_keys: ['region'],
filter_values: { region: ['Gotland'] },
result_sample: [{ customer: 'Anna Andersson', revenue: 42 }],
metric_name: 'revenue',
dashboard_id: 'dash_sales',
widget_count: 4,
};
it('emits none of the forbidden material', () => {
const events = biUsageEventsForExecution(hostile);
const serialized = JSON.stringify(events);
// The detector must be able to find these strings — they are in the input.
const input = JSON.stringify(hostile);
for (const secret of ['19800101-1234', 'Anna Andersson', 'Gotland', 't_alpha', 'SELECT *']) {
expect(input, `detector check for ${secret}`).toContain(secret);
}
for (const secret of [
'19800101-1234',
'Anna Andersson',
'Gotland',
't_alpha',
'SELECT *',
'duplicate key',
]) {
expect(serialized, secret).not.toContain(secret);
}
});
it('every emitted key is on the allowlist AUD0 accepts', () => {
const events = biUsageEventsForExecution(hostile);
for (const e of events) {
for (const key of Object.keys(e.payload)) {
expect(EMITTABLE_PAYLOAD_KEYS, `${e.event_type}.${key}`).toContain(key);
}
}
});
it('a query_hash that is not a sha256 digest is dropped, not reshaped', () => {
const events = biUsageEventsForExecution({ ...hostile, compiled_query_hash: 'SELECT 1' });
for (const e of events) expect(e.payload.query_hash).toBeUndefined();
});
it('a running query emits only "started" — it is neither a success nor a failure', () => {
const events = biUsageEventsForExecution({
...hostile,
state: 'running',
metric_name: undefined,
dashboard_id: undefined,
});
expect(events.map((e) => e.event_type)).toEqual(['aud0.analytics_query_started']);
});
it('a cancelled query is reported as failed with a CODE', () => {
const events = biUsageEventsForExecution({
...hostile,
state: 'cancelled',
error_code: undefined,
metric_name: undefined,
dashboard_id: undefined,
});
const failed = events.find((e) => e.event_type === 'aud0.analytics_query_failed');
expect(failed?.payload.error_code).toBe('CANCELLED');
expect(failed?.payload.error_message).toBeUndefined();
});
});