Files
0pt/tests/analytics/analytics-engine-contract.test.ts
T
Claude Opus 5 84bb9faa4f
CI (SIAX Cloud) / security (push) Successful in 41s
Security Scan (SIAX Cloud) / security (push) Successful in 40s
CI (SIAX Cloud) / integration (push) Successful in 52s
CI (SIAX Cloud) / quality (push) Successful in 1m5s
CI (SIAX Cloud) / security (pull_request) Successful in 31s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 29s
CI (SIAX Cloud) / integration (pull_request) Successful in 45s
CI (SIAX Cloud) / quality (pull_request) Successful in 53s
feat(analytics): BI-P002..BI-P012 analytics core — domain model, tiers, contracts, Arrow, ClickHouse engine, isolation, governance
Implements the eight BI program prompts that make 0pt the canonical SIAX
analytics capability. Every file is NEW: not one existing file is modified,
and src/analytics/clickhouse-analytics.ts is byte-identical to main.

BI-P002 — the nineteen canonical entities, verbatim, with tenant_id +
project_id on every tenant-owned record. sql/008 gives each one a table.
The four version entities BI-P002 names as immutable are enforced by BEFORE
UPDATE/DELETE triggers, not by a TypeScript `readonly` — proven by direct SQL
against a live Postgres.

BI-P003 — RAW/NORMALIZED/CURATED/SERVING with the read rule enforced:
assertReadableByBiSurface refuses a RAW dataset to a dashboard unless a
recorded exception exists. All eight required declarations are NOT NULL.

BI-P004 — DatasetContract and a fail-closed compatibility gate. Fifteen
breaking-change classes are each tested twice: refused under a minor bump,
accepted under a major one.

BI-P005 — Arrow IPC alongside JSON, with a hand-written FlatBuffers writer so
no npm dependency (or CVE surface) is added. Conformance comes from three
independent implementations, not from self-agreement: pyarrow 25.0.1 and
DuckDB 1.5.5 read identical values from both ClickHouse's Arrow and ours, and
ClickHouse's own Arrow C++ ingests our 20,000-row batch with exact aggregates.

BI-P009 — SiaxAnalyticsEngine with all eight capabilities. The existing
ClickHouseAnalytics is COMPOSED, never replaced. executeQuery (analyst SQL) is
declared and deliberately refused with CAPABILITY_UNAVAILABLE until BI-P017 and
BI-P033 land, rather than opening the raw-SQL hole BI-P011 enumerates.

BI-P010 — four purpose-built tables, not one giant one, and the difference is
measured against the counterfactual: 48.8ms/40,960 rows vs 174.4ms/5,400,000
rows for the same question; 5/660 granules vs 660/1149.

BI-P011 — layered isolation, each layer proven with the layer above removed.
Without the forced predicate the same statement returns 50,050 (both tenants);
with it, t_alpha gets 50. Without a row policy an unrestricted user sees 2
tenants; the restricted user running the IDENTICAL statement sees 1.

BI-P012 — every named limit, fired individually and identified by ClickHouse
error code, plus the half usually skipped: an unrelated tenant is timed while
a real CPU bomb is in flight and must still return the CORRECT answer.

Three defects found by writing tests that can fail:
  * `CHECK (array_length(purposes,1) >= 1)` can NEVER fail — array_length is
    NULL for '{}' and a NULL CHECK passes. Fixed with coalesce(); the test
    that caught it is pinned.
  * EXPLAIN ESTIMATE run under the caller's own max_rows_to_read fails with
    code 158 exactly when the estimate would be over budget. Planner calls now
    use a narrower envelope.
  * result_overflow_mode=break returns HTTP 200 with 65,409 of 100,000 rows
    and no indication it is short. Every SELECT ships `throw`.

Verified rather than assumed: server7's ClickHouse holds `opt_scraped` (51
tables, 407,793,471 rows, `persons`/`cz_owners` included) — the b0ts plane this
module is prohibited from touching — has no `opt_analytics` database, zero
tenant_id columns and zero row policies. Nothing there was modified; every
proof ran against disposable containers.

Evidence: evidence/bi-p002-p012/{test-transcript.txt,bi-p010-benchmark.json,
bi-p005-arrow-roundtrip.json,server7-clickhouse-probe.md}
Docs: docs/analytics/BI_ANALYTICS_CORE.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 23:01:27 +02:00

125 lines
5.3 KiB
TypeScript

/**
* BI-P009 DoD: "Provider contract tests added."
*
* This is the contract every `SiaxAnalyticsEngine` provider must satisfy
* regardless of engine — ClickHouse today, DuckDB (BI-P014) and Trino
* (BI-P015) later. It is written against the INTERFACE, so a future provider
* imports `analyticsEngineContract` and gets the same bar.
*
* The identity rules here are the ones that matter most: this estate has
* already shipped a live cross-tenant hole by reading the tenant out of a
* REQUEST BODY (INF0 Wave 2). So the contract asserts that a request field
* named `tenant_id` cannot influence the engine at all.
*/
import { describe, it, expect } from "vitest";
import { ClickHouseAnalyticsEngine } from "@/src/analytics/engine/clickhouse-engine";
import {
SIAX_ANALYTICS_ENGINE_CAPABILITIES,
type AnalyticsIdentity,
type SiaxAnalyticsEngine,
} from "@/src/analytics/engine/siax-analytics-engine";
const IDENTITY: AnalyticsIdentity = {
tenant_id: "t_alpha",
project_id: "p_default",
principal_id: "usr_1",
};
/** Reusable contract suite — future providers call this with their factory. */
export function analyticsEngineContract(name: string, make: () => SiaxAnalyticsEngine): void {
describe(`SiaxAnalyticsEngine contract: ${name}`, () => {
it("implements all eight BI-P009 capabilities as callable methods", () => {
const engine = make() as unknown as Record<string, unknown>;
for (const cap of SIAX_ANALYTICS_ENGINE_CAPABILITIES) {
expect(typeof engine[cap], `${cap} is not a method`).toBe("function");
}
expect(SIAX_ANALYTICS_ENGINE_CAPABILITIES).toEqual([
"health",
"executeMetric",
"executeQuery",
"estimateQuery",
"cancelQuery",
"explain",
"ingestProjection",
"refreshProjection",
]);
});
it("names its provider", () => {
expect(make().provider.length).toBeGreaterThan(0);
});
it("refuses every tenant-scoped capability without a verified identity", async () => {
const engine = make();
const blank = { tenant_id: "", project_id: "p", principal_id: "usr_1" };
const noPrincipal = { tenant_id: "t_alpha", project_id: "p", principal_id: "" };
for (const bad of [blank, noPrincipal]) {
await expect(engine.executeMetric(bad, { metric: "m", aggregate: "sum" })).rejects.toMatchObject({
code: "UNAUTHENTICATED",
});
await expect(engine.estimateQuery(bad, { metric: "m", aggregate: "sum" })).rejects.toMatchObject({
code: "UNAUTHENTICATED",
});
await expect(engine.ingestProjection(bad, { dataset_id: "ds", rows: [] })).rejects.toMatchObject({
code: "UNAUTHENTICATED",
});
await expect(engine.refreshProjection(bad, "ds")).rejects.toMatchObject({ code: "UNAUTHENTICATED" });
await expect(engine.cancelQuery(bad, "q")).rejects.toMatchObject({ code: "UNAUTHENTICATED" });
}
});
it("a tenant_id smuggled into the REQUEST cannot influence the engine (INF0 Wave-2 bug class)", async () => {
const engine = make();
const smuggled = { metric: "m", aggregate: "sum", tenant_id: "t_victim" } as never;
// The request type has no tenant field; if a provider ever started
// reading one, this call would target t_victim. The engine must still
// fail on the IDENTITY, proving the request field is inert.
await expect(
engine.executeMetric({ tenant_id: "", project_id: "p", principal_id: "u" }, smuggled),
).rejects.toMatchObject({ code: "UNAUTHENTICATED" });
});
it("cancelQuery refuses a query id belonging to another tenant", async () => {
const engine = make();
await expect(engine.cancelQuery(IDENTITY, "opt_deadbeefdead_someoneelses")).rejects.toMatchObject({
code: "AUTHORIZATION_DENIED",
});
});
it("health() reports unhealthy rather than throwing when the engine is unreachable", async () => {
const report = await make().health();
expect(report.healthy).toBe(false);
expect(report.provider.length).toBeGreaterThan(0);
// Never leak an engine body or a DSN into the operator-facing detail.
expect(report.detail ?? "").not.toMatch(/http|password|@/i);
});
});
}
analyticsEngineContract(
"clickhouse (unreachable host — contract-level assertions only)",
() =>
new ClickHouseAnalyticsEngine({
baseUrl: "http://clickhouse.invalid:8123",
database: "opt_contract_test",
timeoutMs: 500,
}),
);
describe("BI-P009 refusals that are deliberate, not missing", () => {
it("executeQuery (analyst SQL) is declared and REFUSED until BI-P017/BI-P033", async () => {
const engine = new ClickHouseAnalyticsEngine({ baseUrl: "http://clickhouse.invalid:8123" });
await expect(engine.executeQuery(IDENTITY, { sql: "SELECT 1", dataset_ids: [] })).rejects.toMatchObject({
code: "CAPABILITY_UNAVAILABLE",
});
});
it("the PRESERVED ClickHouseAnalytics is reachable through the engine, not replaced by it", () => {
const engine = new ClickHouseAnalyticsEngine({ baseUrl: "http://clickhouse.invalid:8123" });
expect(engine.legacyAnalytics.constructor.name).toBe("ClickHouseAnalytics");
expect(typeof engine.legacyAnalytics.queryMetric).toBe("function");
expect(typeof engine.legacyAnalytics.ensureSchema).toBe("function");
expect(typeof engine.legacyAnalytics.ingest).toBe("function");
});
});