CI (SIAX Cloud) / integration (push) Failing after 22s
CI (SIAX Cloud) / security (push) Successful in 22s
Security Scan (SIAX Cloud) / security (push) Successful in 21s
CI (SIAX Cloud) / quality (push) Successful in 43s
CI (SIAX Cloud) / integration (pull_request) Failing after 3s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / quality (pull_request) Successful in 43s
- tests/integration/: 10 tests against REAL OpenSearch 2.11 (tenant isolation, purpose/rights filters, generation rebuild/rollback, projection-disaster rebuild §68) + REAL ClickHouse 24.8 (tenant-isolated metric DSL, TTL schema, injection-surface refusals). Honest skip without env - and CI fails hard if the suites skip instead of running. - .gitea/workflows/ci.yml: new blocking 'integration' job with self-identifying service containers (st0re ci.yml's measured alias-collision discipline: run-unique cluster.name / credentials, getent-enumerated IP selection, no-match = hard fail). - STATUS.md: full 8-level Master-DoD classification per capability incl. the honest gaps blocking PRODUCTION_READY. - dependency-mode: NEXT_PHASE=phase-production-build is not a serving runtime (next build page-data collection must compile; guard re-arms at boot) - pinned by contract test. - docs/02-architecture.md: superseded the accurate-but-now-fixed 'every client runs against its mock' paragraph; .env.example: client URLs now actually read from env + new P0 backend vars. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
97 lines
4.0 KiB
TypeScript
97 lines
4.0 KiB
TypeScript
/**
|
|
* REAL ClickHouse integration test (Master-DoD §48/§50) for the Analytics
|
|
* capability. Runs against CLICKHOUSE_URL (+ CLICKHOUSE_AUTH "user:pass"
|
|
* when the server requires it); SKIPS HONESTLY when unset.
|
|
*
|
|
* Proves against the real engine: schema creation with TTL retention,
|
|
* JSONEachRow ingest, the closed metric-query DSL, and — the §69-relevant
|
|
* part — that the FORCED tenant predicate isolates tenants on real data.
|
|
*/
|
|
import { describe, it, expect, beforeAll } from "vitest";
|
|
import { ClickHouseAnalytics } from "@/src/analytics/clickhouse-analytics";
|
|
|
|
const CLICKHOUSE_URL = process.env.CLICKHOUSE_URL;
|
|
const run = CLICKHOUSE_URL ? describe : describe.skip;
|
|
|
|
const DB = `opt_itest_${Date.now().toString(36)}`;
|
|
|
|
run("Analytics capability against a LIVE ClickHouse", () => {
|
|
// The skip-variant still evaluates this callback body, so the constructor
|
|
// must tolerate the env-less case (the tests themselves never run then).
|
|
const analytics = new ClickHouseAnalytics({
|
|
baseUrl: CLICKHOUSE_URL ?? "http://skipped.invalid",
|
|
basicAuth: process.env.CLICKHOUSE_AUTH,
|
|
database: DB,
|
|
});
|
|
|
|
beforeAll(async () => {
|
|
expect(await analytics.health()).toBe(true);
|
|
await analytics.ensureSchema(30);
|
|
// ensureSchema is idempotent (CREATE IF NOT EXISTS).
|
|
await analytics.ensureSchema(30);
|
|
}, 60_000);
|
|
|
|
it("ingests events and answers aggregate metric queries", async () => {
|
|
const res = await analytics.ingest([
|
|
{ tenant_id: "t_alpha", dataset_id: "ds_companies", metric: "rows_ingested", value: 100 },
|
|
{ tenant_id: "t_alpha", dataset_id: "ds_companies", metric: "rows_ingested", value: 50 },
|
|
{ tenant_id: "t_alpha", dataset_id: "ds_vehicles", metric: "rows_ingested", value: 7 },
|
|
{ tenant_id: "t_beta", dataset_id: "ds_companies", metric: "rows_ingested", value: 999 },
|
|
]);
|
|
expect(res.ingested).toBe(4);
|
|
|
|
const total = await analytics.queryMetric({
|
|
tenant_id: "t_alpha",
|
|
metric: "rows_ingested",
|
|
aggregate: "sum",
|
|
});
|
|
expect(total).toEqual([{ value: 157 }]);
|
|
|
|
const perDataset = await analytics.queryMetric({
|
|
tenant_id: "t_alpha",
|
|
dataset_id: "ds_companies",
|
|
metric: "rows_ingested",
|
|
aggregate: "sum",
|
|
});
|
|
expect(perDataset).toEqual([{ value: 150 }]);
|
|
}, 30_000);
|
|
|
|
it("REAL tenant isolation: t_beta's numbers never leak into t_alpha aggregates", async () => {
|
|
const alpha = await analytics.queryMetric({ tenant_id: "t_alpha", metric: "rows_ingested", aggregate: "max" });
|
|
expect(alpha[0].value).toBe(100); // NOT 999
|
|
const beta = await analytics.queryMetric({ tenant_id: "t_beta", metric: "rows_ingested", aggregate: "sum" });
|
|
expect(beta[0].value).toBe(999);
|
|
const ghost = await analytics.queryMetric({ tenant_id: "t_gamma", metric: "rows_ingested", aggregate: "count" });
|
|
expect(ghost[0].value).toBe(0);
|
|
});
|
|
|
|
it("group-by dimension queries work through the closed DSL", async () => {
|
|
await analytics.ingest([
|
|
{ tenant_id: "t_alpha", dataset_id: "ds_x", metric: "dq_score", value: 90, dimensions: { severity: "warning" } },
|
|
{ tenant_id: "t_alpha", dataset_id: "ds_x", metric: "dq_score", value: 40, dimensions: { severity: "critical" } },
|
|
]);
|
|
const rows = await analytics.queryMetric({
|
|
tenant_id: "t_alpha",
|
|
metric: "dq_score",
|
|
aggregate: "avg",
|
|
group_by: "severity",
|
|
});
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows.find((r) => r.group === "warning")?.value).toBe(90);
|
|
});
|
|
|
|
it("rejects tenant-less queries and invalid group_by identifiers (injection surface closed)", async () => {
|
|
await expect(
|
|
analytics.queryMetric({ tenant_id: " ", metric: "x", aggregate: "sum" })
|
|
).rejects.toMatchObject({ code: "INVALID_REQUEST" });
|
|
await expect(
|
|
analytics.queryMetric({
|
|
tenant_id: "t_alpha",
|
|
metric: "x",
|
|
aggregate: "sum",
|
|
group_by: "sev'; DROP TABLE opt_metric_events; --",
|
|
})
|
|
).rejects.toMatchObject({ code: "INVALID_REQUEST" });
|
|
});
|
|
});
|