Files
0pt/tests/analytics/quality-and-freshness.test.ts
T
Claude Opus 5 6cb00add57
CI (SIAX Cloud) / integration (push) Successful in 43s
CI (SIAX Cloud) / security (push) Successful in 23s
CI (SIAX Cloud) / quality (push) Successful in 57s
Security Scan (SIAX Cloud) / security (push) Successful in 21s
CI (SIAX Cloud) / security (pull_request) Successful in 23s
CI (SIAX Cloud) / integration (pull_request) Successful in 40s
CI (SIAX Cloud) / quality (pull_request) Successful in 55s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 25s
feat(analytics): BI-P025/BI-P026 — quality providers that fail on empty, freshness that refuses to be silent
Both prompts are about a FALSE GREEN, so both are tested by constructing the
false green and requiring a failure.

BI-P025. Three provider kinds, and the interface makes the difference visible
instead of implying parity: `providerKind` is "executing" or "ingesting". The
native provider runs checks; the dbt and Great Expectations adapters INGEST
results produced elsewhere (n0d#38 owns dbt execution against BI-P022, and GE
is Python — nothing here runs it). Calling all three "providers" while only
one executes is how "we have Great Expectations coverage" ends up in a status
report.

THE FALSE GREEN, proven against a live ClickHouse 24.8.14.39: a `not_null`
check over an EMPTY table returns violations=0, evaluated=0. The raw counts
are asserted first — the engine really does say zero-over-zero — and then the
provider is required to call it a FAILURE, not a pass. Every rule compiles to
a VIOLATION expression rather than a "rows that are fine" count, so the
distinction cannot be lost at the runner layer.

Same treatment for the ingesting providers: a MISSING dbt result is a failure,
and dbt's `error`, `warn` and `skipped` are each asserted not to be passes.
`skipped` in particular is the exact shape of l0re's 28 silently-skipped
Postgres tests.

The gate: zero verdicts QUARANTINES. "No rules ran" and "everything passed"
produce the same green in most implementations, which is how a quality gate
stops gating without anyone noticing — api0's `trivy --exit-code 0` in another
costume. A later passing run does not auto-release either; release takes a
principal and a reason, or a flapping rule would quarantine and un-quarantine
a dataset between two reads of the same dashboard.

BI-P025's DoD is the READ side: a quarantined projection is REFUSED, not
annotated. An annotation on a dashboard nobody reads is not a quarantine.

Live counts, all exact, from evidence/bi-p025/native-quality-live.json:
not_null 5/118, range 3, distribution 4, unique 11. The unique figure is 11
rather than the 6 the first version of the test asserted — the five
empty-string customers are duplicates of each other, so not_null and unique
overlap on the same defect. The engine was right and the fixture arithmetic
was wrong; the breakdown is written into the test rather than the number
quietly adjusted.

Tenant isolation re-proven on this path specifically, because a quality run
uses an ADMINISTRATIVE credential: with 618 rows in the base table, t_alpha
evaluates 118 and t_beta 500, and t_beta's blocking failure quarantines t_beta
alone. Without the forced predicate one tenant's dirty rows would quarantine
another tenant's projection.

BI-P026. Five states, and the DoD's operative word is "silently". So the gate
sits at the boundary every result crosses: a result with NO freshness envelope
is unreleasable — not defaulted to FRESH, not defaulted to UNKNOWN — because
"we forgot to attach it" and "the data is current" are indistinguishable to a
reader, and any default makes forgetting the silent path. A non-current state
without an explicit acknowledgement is refused, so a consumer that ignores the
envelope cannot render stale numbers as current.

FAILED outranks age: a pipeline that just failed may still hold
recently-timestamped data from the previous run, and reporting that as FRESH
is how a broken pipeline stays invisible for a day. UNKNOWN is never collapsed
into FRESH.

54 unit tests, 11 live tests. typecheck + eslint clean.

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

378 lines
15 KiB
TypeScript

/**
* tests/analytics/quality-and-freshness.test.ts
* ==============================================
* BI-P025 (data quality provider) and BI-P026 (data freshness).
*
* Both DoDs are about a FALSE GREEN, so both are tested by constructing the
* false green and requiring a failure:
*
* BI-P025 A quality gate that passes when nothing ran. A `not_null` check
* over an EMPTY table finds zero violations — the single most
* common false green in data quality. Both are asserted to FAIL.
*
* BI-P026 UNKNOWN age quietly rendering as current, and a result released
* with no freshness envelope at all. Both are refused.
*/
import { describe, expect, it } from 'vitest';
import { SiaxError } from '@/src/platform/errors';
import {
DbtQualityProvider,
GreatExpectationsQualityProvider,
NativeQualityProvider,
QUALITY_RULES,
type QualityQueryRunner,
type QualityRuleSpec,
type QualityVerdict,
} from '@/src/analytics/quality/quality-provider';
import { InMemoryQuarantineStore, QualityGate } from '@/src/analytics/quality/quarantine';
import {
assertFreshnessDisclosed,
computeFreshness,
FRESHNESS_STATES,
isPresentableAsCurrent,
NON_CURRENT_STATES,
} from '@/src/analytics/quality/freshness';
function runner(
violations: number,
evaluated: number,
): QualityQueryRunner & { lastExpression?: string; lastParams?: Record<string, string> } {
const r: QualityQueryRunner & { lastExpression?: string; lastParams?: Record<string, string> } = {
engine: 'clickhouse',
async count(args) {
r.lastExpression = args.violationExpression;
r.lastParams = args.params;
return { violations, evaluated };
},
};
return r;
}
const RULE: QualityRuleSpec = {
rule_id: 'q_not_null_customer',
tenant_id: 't_alpha',
dataset_id: 'ds_sales',
rule: 'not_null',
field: 'customer',
severity: 'block',
params: {},
};
describe('BI-P025 — the native provider EXECUTES, and says so', () => {
it('declares every rule BI-P025 names', () => {
expect([...QUALITY_RULES]).toEqual([
'not_null',
'unique',
'range',
'referential',
'freshness',
'schema',
'distribution',
'custom',
]);
});
it('passes a clean dataset', async () => {
const verdict = await new NativeQualityProvider(runner(0, 1000)).evaluate(RULE);
expect(verdict.passed).toBe(true);
expect(verdict.provider_kind).toBe('executing');
});
it('FAILS an empty dataset instead of reporting zero violations', async () => {
// The false green this whole file exists for: a not-null check over zero
// rows finds zero violations and looks healthy while the pipeline is dead.
const verdict = await new NativeQualityProvider(runner(0, 0)).evaluate(RULE);
expect(verdict.passed).toBe(false);
expect(verdict.detail).toContain('EMPTY');
});
it('fails a dataset with violations', async () => {
const verdict = await new NativeQualityProvider(runner(7, 1000)).evaluate(RULE);
expect(verdict.passed).toBe(false);
expect(verdict.violation_count).toBe(7);
});
it('binds rule parameters instead of splicing them into the expression', async () => {
const r = runner(0, 10);
await new NativeQualityProvider(r).evaluate({
...RULE,
rule: 'distribution',
field: 'channel',
params: { accepted_values: ['web', "retail'; DROP TABLE x; --"] },
});
expect(r.lastExpression).not.toContain('DROP');
expect(Object.values(r.lastParams ?? {})).toContain("retail'; DROP TABLE x; --");
});
it('refuses a field name that is not an identifier', async () => {
const p = new NativeQualityProvider(runner(0, 10));
await expect(p.evaluate({ ...RULE, field: 'customer; DROP TABLE x' })).rejects.toBeInstanceOf(SiaxError);
});
it('refuses an incomplete rule rather than evaluating a meaningless one', async () => {
const p = new NativeQualityProvider(runner(0, 10));
await expect(p.evaluate({ ...RULE, rule: 'range', params: {} })).rejects.toBeInstanceOf(SiaxError);
await expect(p.evaluate({ ...RULE, rule: 'freshness', params: {} })).rejects.toBeInstanceOf(SiaxError);
await expect(p.evaluate({ ...RULE, rule: 'distribution', params: {} })).rejects.toBeInstanceOf(SiaxError);
await expect(p.evaluate({ ...RULE, rule: 'custom', params: {} })).rejects.toBeInstanceOf(SiaxError);
});
it('says CAPABILITY_UNAVAILABLE for rules it cannot evaluate, rather than passing them', async () => {
const p = new NativeQualityProvider(runner(0, 10));
for (const rule of ['schema', 'referential'] as const) {
await expect(p.evaluate({ ...RULE, rule })).rejects.toBeInstanceOf(SiaxError);
expect(p.supports(rule)).toBe(false);
}
});
});
describe('BI-P025 — the ingesting providers do not pretend to execute', () => {
it('dbt and GE declare themselves as ingesting', async () => {
expect(new DbtQualityProvider(new Map()).providerKind).toBe('ingesting');
expect(new GreatExpectationsQualityProvider(new Map()).providerKind).toBe('ingesting');
});
it('a MISSING dbt result is a failure, not a pass', async () => {
// Treating "no result" as "passed" is how a gate stops gating the moment
// the test stops running.
const verdict = await new DbtQualityProvider(new Map()).evaluate(RULE);
expect(verdict.passed).toBe(false);
expect(verdict.detail).toContain('no dbt result');
});
it.each(['error', 'warn', 'skipped'] as const)('dbt status %s is NOT a pass', async (status) => {
const results = new Map([[RULE.rule_id, { unique_id: 't1', status, failures: 0 }]]);
const verdict = await new DbtQualityProvider(results).evaluate(RULE);
expect(verdict.passed).toBe(false);
});
it('dbt pass is a pass', async () => {
const results = new Map([[RULE.rule_id, { unique_id: 't1', status: 'pass' as const, failures: 0 }]]);
expect((await new DbtQualityProvider(results).evaluate(RULE)).passed).toBe(true);
});
it('a missing GE suite is a failure', async () => {
expect((await new GreatExpectationsQualityProvider(new Map()).evaluate(RULE)).passed).toBe(false);
});
it("a GE suite's unexpected counts are carried through", async () => {
const suites = new Map([
[
RULE.rule_id,
{
expectation_suite_name: 'sales_suite',
success: false,
results: [
{
expectation_config: { expectation_type: 'expect_column_values_to_not_be_null' },
success: false,
result: { unexpected_count: 12, element_count: 1000 },
},
],
},
],
]);
const verdict = await new GreatExpectationsQualityProvider(suites).evaluate(RULE);
expect(verdict.passed).toBe(false);
expect(verdict.violation_count).toBe(12);
expect(verdict.evaluated_rows).toBe(1000);
});
});
describe('BI-P025 DoD — a failed critical gate QUARANTINES the serving projection', () => {
function verdict(overrides: Partial<QualityVerdict> = {}): QualityVerdict {
return {
rule_id: 'q1',
dataset_id: 'ds_sales',
tenant_id: 't_alpha',
rule: 'not_null',
severity: 'block',
passed: true,
violation_count: 0,
evaluated_rows: 100,
produced_by: 'native:clickhouse',
provider_kind: 'executing',
evaluated_at: new Date().toISOString(),
...overrides,
};
}
it('a blocking failure quarantines, and the READ is then REFUSED', async () => {
const gate = new QualityGate(new InMemoryQuarantineStore());
// Before: readable.
await expect(gate.assertProjectionReadable('t_alpha', 'ds_sales')).resolves.toBeUndefined();
const outcome = await gate.apply('t_alpha', 'ds_sales', [verdict({ passed: false, severity: 'block' })]);
expect(outcome.quarantined).toBe(true);
expect(outcome.passed).toBe(false);
// After: refused. Not annotated — refused.
await expect(gate.assertProjectionReadable('t_alpha', 'ds_sales')).rejects.toBeInstanceOf(SiaxError);
});
it('a WARN failure does not quarantine, but is surfaced', async () => {
const gate = new QualityGate(new InMemoryQuarantineStore());
const outcome = await gate.apply('t_alpha', 'ds_sales', [verdict({ passed: false, severity: 'warn' })]);
expect(outcome.quarantined).toBe(false);
expect(outcome.warnings).toHaveLength(1);
await expect(gate.assertProjectionReadable('t_alpha', 'ds_sales')).resolves.toBeUndefined();
});
it("a gate over ZERO verdicts QUARANTINES — 'no rules ran' is not 'everything passed'", async () => {
// The same shape as l0re's silently-skipped tests and api0's
// `trivy --exit-code 0`: a check that produced nothing reporting green.
const gate = new QualityGate(new InMemoryQuarantineStore());
const outcome = await gate.apply('t_alpha', 'ds_sales', []);
expect(outcome.passed).toBe(false);
expect(outcome.quarantined).toBe(true);
await expect(gate.assertProjectionReadable('t_alpha', 'ds_sales')).rejects.toBeInstanceOf(SiaxError);
});
it('a later passing run does NOT auto-release a quarantine', async () => {
const gate = new QualityGate(new InMemoryQuarantineStore());
await gate.apply('t_alpha', 'ds_sales', [verdict({ passed: false })]);
await gate.apply('t_alpha', 'ds_sales', [verdict({ passed: true })]);
// A flapping rule would otherwise quarantine and un-quarantine the dataset
// between two reads of the same dashboard.
await expect(gate.assertProjectionReadable('t_alpha', 'ds_sales')).rejects.toBeInstanceOf(SiaxError);
});
it('release requires a principal and a reason', async () => {
const gate = new QualityGate(new InMemoryQuarantineStore());
await gate.apply('t_alpha', 'ds_sales', [verdict({ passed: false })]);
await expect(gate.release('t_alpha', 'ds_sales', '', 'x')).rejects.toBeInstanceOf(SiaxError);
await expect(gate.release('t_alpha', 'ds_sales', 'u_1', ' ')).rejects.toBeInstanceOf(SiaxError);
const released = await gate.release('t_alpha', 'ds_sales', 'u_1', 'upstream backfilled');
expect(released.released_by).toBe('u_1');
await expect(gate.assertProjectionReadable('t_alpha', 'ds_sales')).resolves.toBeUndefined();
});
it("quarantine is tenant-scoped: one tenant's failure does not block another", async () => {
const gate = new QualityGate(new InMemoryQuarantineStore());
await gate.apply('t_alpha', 'ds_sales', [verdict({ passed: false })]);
await expect(gate.assertProjectionReadable('t_beta', 'ds_sales')).resolves.toBeUndefined();
});
it('refuses a verdict belonging to another tenant or dataset', async () => {
const gate = new QualityGate(new InMemoryQuarantineStore());
await expect(
gate.apply('t_alpha', 'ds_sales', [verdict({ tenant_id: 't_beta' })]),
).rejects.toBeInstanceOf(SiaxError);
await expect(
gate.apply('t_alpha', 'ds_sales', [verdict({ dataset_id: 'ds_other' })]),
).rejects.toBeInstanceOf(SiaxError);
});
});
describe('BI-P026 — five states, and UNKNOWN is not FRESH', () => {
const HOUR = 3_600_000;
const NOW = Date.parse('2026-09-07T12:00:00Z');
const policy = { expectedIntervalMs: HOUR };
it('declares the five states BI-P026 names', () => {
expect([...FRESHNESS_STATES]).toEqual(['FRESH', 'DELAYED', 'STALE', 'FAILED', 'UNKNOWN']);
});
it('only FRESH may be presented as current', () => {
expect(isPresentableAsCurrent('FRESH')).toBe(true);
for (const s of NON_CURRENT_STATES) expect(isPresentableAsCurrent(s)).toBe(false);
expect(NON_CURRENT_STATES).toContain('UNKNOWN');
});
const cases: Array<[string, Parameters<typeof computeFreshness>[0], string]> = [
[
'recent load',
{ lastSuccessfulLoadAt: '2026-09-07T11:50:00Z', lastRunStatus: 'succeeded', policy, now: NOW },
'FRESH',
],
[
'behind schedule',
{ lastSuccessfulLoadAt: '2026-09-07T09:50:00Z', lastRunStatus: 'succeeded', policy, now: NOW },
'DELAYED',
],
[
'well past the threshold',
{ lastSuccessfulLoadAt: '2026-09-06T12:00:00Z', lastRunStatus: 'succeeded', policy, now: NOW },
'STALE',
],
[
'the last run FAILED, even though the data looks recent',
{ lastSuccessfulLoadAt: '2026-09-07T11:59:00Z', lastRunStatus: 'failed', policy, now: NOW },
'FAILED',
],
['never run', { lastRunStatus: 'never_run', policy, now: NOW }, 'UNKNOWN'],
['no load timestamp at all', { policy, now: NOW }, 'UNKNOWN'],
['an unparseable timestamp', { lastSuccessfulLoadAt: 'not a date', policy, now: NOW }, 'UNKNOWN'],
];
it.each(cases)('%s -> %s', (_label, input, expected) => {
expect(computeFreshness(input).state).toBe(expected);
});
it('FAILED outranks a recent timestamp — a broken pipeline must not look FRESH', () => {
const env = computeFreshness({
lastSuccessfulLoadAt: '2026-09-07T11:59:59Z',
lastRunStatus: 'failed',
policy,
now: NOW,
});
expect(env.state).toBe('FAILED');
expect(env.disclosure).toContain('FAILED');
});
it('every state carries a disclosure a UI can render verbatim', () => {
for (const [, input] of cases) {
const env = computeFreshness(input);
expect(env.disclosure.length).toBeGreaterThan(10);
}
});
});
describe('BI-P026 DoD — stale data is never SILENTLY presented as current', () => {
const env = computeFreshness({
lastSuccessfulLoadAt: '2026-09-01T00:00:00Z',
lastRunStatus: 'succeeded',
policy: { expectedIntervalMs: 3_600_000 },
now: Date.parse('2026-09-07T12:00:00Z'),
});
it('refuses to release a result with NO freshness envelope', () => {
// Not defaulted to FRESH, not defaulted to UNKNOWN: unreleasable. "We
// forgot to attach it" and "the data is current" are indistinguishable to
// a reader, so a default would make forgetting the silent path.
expect(() => assertFreshnessDisclosed({}, 'dashboard panel p1')).toThrowError(SiaxError);
});
it('refuses to release STALE data that the consumer has not acknowledged', () => {
expect(env.state).toBe('STALE');
let thrown: unknown;
try {
assertFreshnessDisclosed({ freshness: env }, 'dashboard panel p1');
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(SiaxError);
expect((thrown as SiaxError).message).toContain('STALE');
});
it('releases STALE data once the consumer has acknowledged it', () => {
expect(assertFreshnessDisclosed({ freshness: env, acknowledged_non_current: true }, 'p1').state).toBe(
'STALE',
);
});
it.each(NON_CURRENT_STATES)('refuses an unacknowledged %s result', (state) => {
expect(() => assertFreshnessDisclosed({ freshness: { ...env, state } }, 'p1')).toThrowError(SiaxError);
});
it('releases FRESH data without ceremony', () => {
expect(assertFreshnessDisclosed({ freshness: { ...env, state: 'FRESH' } }, 'p1').state).toBe('FRESH');
});
it('refuses an envelope carrying a state that is not one of the five', () => {
expect(() =>
assertFreshnessDisclosed({ freshness: { ...env, state: 'PROBABLY_FINE' as never } }, 'p1'),
).toThrowError(SiaxError);
});
});