Files
0pt/tests/analytics/duckdb-provider.test.ts
T
Claude Opus 5 c864c8c417
CI (SIAX Cloud) / integration (push) Successful in 40s
CI (SIAX Cloud) / quality (push) Successful in 55s
CI (SIAX Cloud) / security (push) Successful in 21s
Security Scan (SIAX Cloud) / security (push) Successful in 22s
CI (SIAX Cloud) / integration (pull_request) Successful in 39s
CI (SIAX Cloud) / quality (pull_request) Successful in 54s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / security (pull_request) Successful in 25s
fix(analytics): revert 18 files reformatted by mistake, and actually apply the Iceberg fix a previous message claimed
Two corrections, both of my own errors.

1. SCOPE CREEP, NOW REVERTED. Running `prettier --write` on whole directories
   reformatted 18 files this branch had no business touching — the entire
   merged BI-P002..P012 core (arrow/, domain/, engine/, repositories/, five
   analytics integration suites) and, absurdly,
   search-opensearch.integration.test.ts, which has nothing to do with this
   work. The changes were purely stylistic quote churn, but they added ~1,400
   lines of noise to the diff, made the PR harder to review honestly, and
   invited conflicts with the four other lanes working in this estate. All 18
   are now byte-identical to origin/main again (verified with `git diff
   origin/main --name-only`, which reports nothing for them). Formatting is now
   applied only to the files this branch actually owns.

2. A COMMIT MESSAGE THAT DESCRIBED A FIX THAT WAS NOT IN THE CODE. Commit
   3ee0230 said the "does NOT ship the Iceberg table" assertion had been moved
   from a filesystem check to `git ls-files`. It had not. The patch anchor used
   double quotes, prettier had already converted that file to single quotes, so
   the replacement silently matched nothing and the script printed its success
   line anyway. The suite then passed — but only because I had just deleted the
   fixture directory, so the OLD filesystem assertion happened to be true.

   That is precisely the failure mode this branch keeps writing tests against:
   a green run that means something different from what it appears to mean. The
   fix is now genuinely applied and verified the way it should have been the
   first time — by running the suite WITH the Iceberg fixture present on disk,
   which is the case that previously failed. It passes, and it now also asserts
   that the parquet/csv fixtures ARE tracked, so a version shipping no fixture
   at all would fail rather than pass vacuously.

957 tests pass with ClickHouse, Postgres and DuckDB live. typecheck, eslint and
prettier clean on the files this branch owns.

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

327 lines
13 KiB
TypeScript

/**
* tests/analytics/duckdb-provider.test.ts
* ========================================
* BI-P014 — the parts provable without a DuckDB, plus the pin on the parts
* that are not.
*
* The live suite (tests/duckdb/duckdb-provider.live.test.ts) only
* runs when DUCKDB_PYTHON is set. On a runner without it, it contributes
* nothing — so the recorded evidence is committed and THIS file, which never
* skips, checks it. In particular it pins the finding that makes layer 2 real:
*
* `allowed_directories` WITHOUT `enable_external_access=false` permits
* /etc/passwd. Measured, recorded, and asserted here — because a comment
* saying so would rot the moment someone "simplified" the bootstrap.
*/
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { SiaxError } from '@/src/platform/errors';
import {
ArtifactMountRegistry,
assertSessionSandboxed,
duckDbSessionBootstrap,
escapeLiteral,
DuckDbAnalyticsProvider,
REQUIRED_SANDBOX_SETTINGS,
type DuckDbRunner,
} from '@/src/analytics/providers/duckdb-provider';
const EVIDENCE_PATH = path.join(process.cwd(), 'evidence/bi-p014/duckdb-sandbox.json');
const FIXTURES = path.join(process.cwd(), 'tests/fixtures/duckdb');
const IDENTITY = { tenant_id: 't_alpha', principal_id: 'u_1' };
const capturingRunner = (): DuckDbRunner & { lastSql?: string; lastBootstrap?: readonly string[] } => {
const r: DuckDbRunner & { lastSql?: string; lastBootstrap?: readonly string[] } = {
engine: 'duckdb',
async run({ bootstrap, sql }) {
r.lastSql = sql;
r.lastBootstrap = bootstrap;
return [];
},
};
return r;
};
describe('BI-P014 — the session bootstrap is one unit, not three settings', () => {
it('emits all three statements, external access LAST-but-one and the lock last', () => {
const stmts = duckDbSessionBootstrap(['/mnt/artifacts']);
expect(stmts).toHaveLength(3);
expect(stmts[0]).toContain('allowed_directories');
expect(stmts[1]).toBe('SET enable_external_access=false');
expect(stmts[2]).toBe('SET lock_configuration=true');
});
it('refuses an EMPTY allow-list — that is not a sandbox', () => {
expect(() => duckDbSessionBootstrap([])).toThrowError(SiaxError);
});
it('refuses a relative or quote-carrying directory', () => {
expect(() => duckDbSessionBootstrap(['relative/path'])).toThrowError(SiaxError);
expect(() => duckDbSessionBootstrap(["/mnt/it's"])).toThrowError(SiaxError);
});
it.each([...REQUIRED_SANDBOX_SETTINGS])(
'assertSessionSandboxed refuses a session missing %s',
(setting) => {
const stmts = duckDbSessionBootstrap(['/mnt/artifacts']).filter((s) => !s.includes(setting));
expect(() => assertSessionSandboxed(stmts)).toThrowError(SiaxError);
},
);
it('refuses a session that sets the allow-list but leaves external access ON', () => {
// The exact naive spelling. It looks configured and permits everything.
const naive = [
"SET allowed_directories=['/mnt/artifacts']",
'SET enable_external_access=true',
'SET lock_configuration=true',
];
expect(() => assertSessionSandboxed(naive)).toThrowError(SiaxError);
});
});
describe('BI-P014 — the mount registry refuses everything that is not a mounted artifact', () => {
function registry(): ArtifactMountRegistry {
// A fake realpath, so path semantics are tested without touching disk.
const fakeReal = (p: string) => (p.includes('symlinked') ? '/etc/passwd' : p);
const reg = new ArtifactMountRegistry('/mnt/artifacts', fakeReal);
reg.register({
artifact_id: 'a_ok',
tenant_id: 't_alpha',
st0re_ref: 'st0re://x',
relative_path: 'sales.parquet',
format: 'parquet',
size_bytes: 10,
});
return reg;
}
it('resolves a mounted artifact for its owner', () => {
expect(registry().resolve(IDENTITY, 'a_ok').absolutePath).toBe('/mnt/artifacts/sales.parquet');
});
it('refuses an absolute mount path', () => {
expect(() =>
registry().register({
artifact_id: 'a_abs',
tenant_id: 't_alpha',
st0re_ref: 'x',
relative_path: '/etc/passwd',
format: 'csv',
size_bytes: 1,
}),
).toThrowError(SiaxError);
});
it('refuses a path that resolves outside the root once symlinks are followed', () => {
expect(() =>
registry().register({
artifact_id: 'a_link',
tenant_id: 't_alpha',
st0re_ref: 'x',
relative_path: 'symlinked.parquet',
format: 'parquet',
size_bytes: 1,
}),
).toThrowError(SiaxError);
});
it("refuses a sibling directory that merely shares the root's prefix", () => {
// `startsWith('/mnt/artifacts')` alone would accept `/mnt/artifacts-evil`.
const reg = new ArtifactMountRegistry('/mnt/artifacts', (p) =>
p.includes('sibling') ? '/mnt/artifacts-evil/x.parquet' : p,
);
expect(() =>
reg.register({
artifact_id: 'a_sib',
tenant_id: 't_alpha',
st0re_ref: 'x',
relative_path: 'sibling.parquet',
format: 'parquet',
size_bytes: 1,
}),
).toThrowError(SiaxError);
});
it('refuses a request with no verified tenant', () => {
expect(() => registry().resolve({ tenant_id: '', principal_id: 'u' }, 'a_ok')).toThrowError(SiaxError);
});
it('distinguishes NOT_FOUND from AUTHORIZATION_DENIED', () => {
const reg = registry();
try {
reg.resolve(IDENTITY, 'a_missing');
} catch (e) {
expect((e as SiaxError).code).toBe('NOT_FOUND');
}
try {
reg.resolve({ tenant_id: 't_beta', principal_id: 'u' }, 'a_ok');
} catch (e) {
// An attempted cross-tenant read must be visible in the audit trail as
// one, not disguised as a missing object.
expect((e as SiaxError).code).toBe('AUTHORIZATION_DENIED');
}
});
it('refuses an artifact id that is not registry-safe', () => {
for (const bad of ['../x', 'a b', "a'b", 'a/b', '']) {
expect(() => registry().resolve(IDENTITY, bad)).toThrowError(SiaxError);
}
});
});
describe('BI-P014 — the query builder has no caller-SQL surface', () => {
function provider(runner: DuckDbRunner, sizeBytes = 10) {
const reg = new ArtifactMountRegistry('/mnt/artifacts', (p) => p);
reg.register({
artifact_id: 'a_ok',
tenant_id: 't_alpha',
st0re_ref: 'x',
relative_path: 'sales.parquet',
format: 'parquet',
size_bytes: sizeBytes,
});
return new DuckDbAnalyticsProvider(reg, runner);
}
it('builds a reader from the RESOLVED path, never from caller text', async () => {
const r = capturingRunner();
await provider(r).query(IDENTITY, { artifact_id: 'a_ok', select: ['country'] });
expect(r.lastSql).toContain("read_parquet('/mnt/artifacts/sales.parquet')");
});
it('always applies the sandbox bootstrap before the query', async () => {
const r = capturingRunner();
await provider(r).query(IDENTITY, { artifact_id: 'a_ok' });
expect(() => assertSessionSandboxed(r.lastBootstrap ?? [])).not.toThrow();
});
it.each(['a b', 'x; DROP TABLE t', '*', '1=1', "a'", '../x'])(
'refuses %s as a column name',
async (col) => {
await expect(
provider(capturingRunner()).query(IDENTITY, { artifact_id: 'a_ok', select: [col] }),
).rejects.toBeInstanceOf(SiaxError);
},
);
it('hands a heavy artifact to N0D instead of running it locally', async () => {
let thrown: unknown;
try {
await provider(capturingRunner(), 40 * 1024 * 1024 * 1024).query(IDENTITY, { artifact_id: 'a_ok' });
} catch (e) {
thrown = e;
}
expect((thrown as SiaxError).code).toBe('CAPABILITY_UNAVAILABLE');
expect((thrown as SiaxError).message).toContain('N0D');
});
it("escapes a literal by DOUBLING the quote, which is DuckDB's only escape", () => {
// MEASURED on DuckDB 1.5.5 rather than assumed, because the whole escaping
// strategy rests on it: `SELECT length('a\\b')` returns 4, so a backslash
// is a literal character; and `SELECT 'a\''` is a PARSER ERROR
// ("unterminated quoted string"), which is what a non-escaping backslash
// produces. A dialect where `\'` closed the literal would make doubling
// insufficient.
expect(escapeLiteral("O'Brien")).toBe("O''Brien");
// A backslash is NOT an escape inside a DuckDB single-quoted string, so it
// must survive untouched rather than being "helpfully" doubled.
expect(escapeLiteral("a\\'b")).toBe("a\\''b");
expect(escapeLiteral("x' OR '1'='1")).toBe("x'' OR ''1''=''1");
});
it('refuses a NUL byte in a filter value rather than escaping it', () => {
expect(() => escapeLiteral('a\u0000b')).toThrowError(SiaxError);
});
it('declares which formats have actually been PROVEN, and why the others have not', () => {
const readiness = provider(capturingRunner()).readiness();
expect(readiness.proven).toContain('parquet');
expect(readiness.proven).toContain('iceberg');
const arrow = readiness.unproven.find((u) => u.format === 'arrow_ipc');
expect(arrow?.reason).toContain('network install');
});
});
describe('BI-P014 — the committed evidence, checked by a test that never skips', () => {
it('the evidence file exists', () => {
expect(
fs.existsSync(EVIDENCE_PATH),
'evidence/bi-p014/duckdb-sandbox.json is missing — run the live suite with DUCKDB_PYTHON set',
).toBe(true);
});
const evidence = fs.existsSync(EVIDENCE_PATH)
? (JSON.parse(fs.readFileSync(EVIDENCE_PATH, 'utf8')) as Record<string, never>)
: ({} as Record<string, never>);
it('was recorded against a real DuckDB', () => {
expect(String(evidence.duckdb_version ?? '')).toMatch(/^\d+\.\d+/);
});
it('PINS the finding: allowed_directories alone read /etc/passwd', () => {
// The load-bearing assertion of this whole file. If someone "simplifies"
// the bootstrap down to the allow-list, the module comment would become a
// lie quietly; this keeps the measurement attached to the build.
const naive = evidence.naive_allowed_directories_is_a_noop as unknown as {
read_etc_passwd_succeeded: boolean;
statements: string[];
};
expect(naive).toBeDefined();
expect(naive.read_etc_passwd_succeeded).toBe(true);
expect(naive.statements.join(' ')).not.toContain('enable_external_access');
});
it('records that NO bypass attempt succeeded with the full bootstrap', () => {
const attempts = evidence.sandbox_bypass_attempts as unknown as Array<{ id: string; succeeded: boolean }>;
expect(Array.isArray(attempts)).toBe(true);
expect(attempts.length).toBeGreaterThanOrEqual(10);
expect(attempts.filter((a) => a.succeeded).map((a) => a.id)).toEqual([]);
});
it('records the counterfactual — the mounted file WAS readable in the same session', () => {
// Without this, "no attempt succeeded" is equally consistent with a
// sandbox that blocks everything, which is an outage, not a control.
expect(evidence.counterfactual_mounted_file_readable).toBe(true);
});
it('records the Iceberg snapshot read, whose fixture is deliberately not committed', () => {
// pyiceberg embeds ABSOLUTE paths in its manifests, so a committed Iceberg
// table would only work on the machine that built it. The recorded result
// is the artefact instead.
const iceberg = evidence.iceberg as unknown as { totals: Record<string, number> };
expect(iceberg?.totals).toEqual({ NO: 20, SE: 40 });
});
it('records that a locked session could not re-open its own sandbox', () => {
const reopened = evidence.reconfiguration_refused as unknown as Array<{ refused: boolean }>;
expect(reopened.length).toBe(3);
expect(reopened.every((r) => r.refused)).toBe(true);
});
});
describe('BI-P014 — the committed fixtures', () => {
it('ships the Parquet and CSV fixtures the live suite reads', () => {
expect(fs.existsSync(path.join(FIXTURES, 'sales.parquet'))).toBe(true);
expect(fs.existsSync(path.join(FIXTURES, 'customers.csv'))).toBe(true);
});
it('does NOT ship the Iceberg table, because its manifests carry absolute paths', () => {
// Asserted against GIT, not the filesystem. The claim is about what is
// COMMITTED; the live suite REBUILDS that table locally, so a filesystem
// check fails whenever both suites run in one pass — it was testing the
// wrong thing rather than catching a real problem.
const tracked = execFileSync('git', ['ls-files', 'tests/fixtures/duckdb/'], {
encoding: 'utf8',
cwd: process.cwd(),
})
.split('\n')
.filter(Boolean);
// The parquet/csv fixtures ARE tracked, so a version that shipped nothing
// would fail here too rather than passing vacuously.
expect(tracked.length).toBeGreaterThan(0);
expect(tracked.filter((f) => f.includes('/iceberg'))).toEqual([]);
});
});