@fabricorg/databricks-testkit 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1256,6 +1256,195 @@ function autoLoaderIngestionCheck(options = {}) {
1256
1256
  }
1257
1257
  };
1258
1258
  }
1259
+ function safeName(prefix) {
1260
+ return `${prefix}_${crypto.randomUUID().replaceAll("-", "_")}`;
1261
+ }
1262
+ function performanceBudgetCheck() {
1263
+ return {
1264
+ id: "performance",
1265
+ description: "SQL Statement Execution p95 stays within the configured latency budget",
1266
+ configured: (env) => Boolean(env.DBX_TEST_PERFORMANCE_P95_MS),
1267
+ async run({ env, warehouse }) {
1268
+ const budgetMs = Number(env.DBX_TEST_PERFORMANCE_P95_MS);
1269
+ const runs = Number(env.DBX_TEST_PERFORMANCE_RUNS ?? 7);
1270
+ if (!Number.isFinite(budgetMs) || budgetMs <= 0) {
1271
+ throw new Error("DBX_TEST_PERFORMANCE_P95_MS must be a positive number");
1272
+ }
1273
+ if (!Number.isInteger(runs) || runs < 3 || runs > 100) {
1274
+ throw new Error("DBX_TEST_PERFORMANCE_RUNS must be an integer from 3 to 100");
1275
+ }
1276
+ await warehouse.query("SELECT 1 AS warmup");
1277
+ const durations = [];
1278
+ for (let index = 0; index < runs; index += 1) {
1279
+ const started = performance.now();
1280
+ await warehouse.query("SELECT 1 AS performance_probe");
1281
+ durations.push(performance.now() - started);
1282
+ }
1283
+ durations.sort((left, right) => left - right);
1284
+ const p95 = durations[Math.ceil(durations.length * 0.95) - 1] ?? 0;
1285
+ if (p95 > budgetMs) {
1286
+ throw new Error(`SQL p95 ${Math.round(p95)}ms exceeded ${budgetMs}ms budget`);
1287
+ }
1288
+ return {
1289
+ runs,
1290
+ p50Ms: Math.round(durations[Math.floor(durations.length * 0.5)] ?? 0),
1291
+ p95Ms: Math.round(p95),
1292
+ budgetMs
1293
+ };
1294
+ }
1295
+ };
1296
+ }
1297
+ function failureRecoveryCheck() {
1298
+ return {
1299
+ id: "failure-recovery",
1300
+ description: "A failed statement is isolated and a subsequent statement recovers",
1301
+ async run({ warehouse }) {
1302
+ let failedAsExpected = false;
1303
+ try {
1304
+ await warehouse.query("SELECT * FROM __fabric_experiments_expected_missing_table__");
1305
+ } catch {
1306
+ failedAsExpected = true;
1307
+ }
1308
+ if (!failedAsExpected) throw new Error("Injected failure unexpectedly succeeded");
1309
+ const rows = await warehouse.query("SELECT 1 AS recovered");
1310
+ if (Number(rows[0]?.recovered) !== 1) {
1311
+ throw new Error("Statement execution did not recover after the injected failure");
1312
+ }
1313
+ return { injectedFailure: true, recovered: true };
1314
+ }
1315
+ };
1316
+ }
1317
+ function secretRotationCheck(options = {}) {
1318
+ return {
1319
+ id: "secret-rotation",
1320
+ description: "Databricks secret put, overwrite, metadata verification, and cleanup",
1321
+ configured: (env) => Boolean(env.DBX_TEST_ROTATION_SECRET_SCOPE),
1322
+ async run({ env, client }) {
1323
+ const scope = env.DBX_TEST_ROTATION_SECRET_SCOPE;
1324
+ const key = options.randomId?.() ?? safeName("fx_rotation_probe");
1325
+ const put = async (value) => client.post("/api/2.0/secrets/put", { scope, key, string_value: value });
1326
+ try {
1327
+ await put(crypto.randomBytes(32).toString("base64url"));
1328
+ await put(crypto.randomBytes(32).toString("base64url"));
1329
+ const listed = await client.get("/api/2.0/secrets/list", {
1330
+ scope
1331
+ });
1332
+ if (!(listed.secrets ?? []).some((secret) => secret.key === key)) {
1333
+ throw new Error(`Rotated secret metadata was not visible in scope ${scope}`);
1334
+ }
1335
+ return { scope, rotated: true, cleanedUp: true };
1336
+ } finally {
1337
+ await client.post("/api/2.0/secrets/delete", { scope, key }).catch(() => void 0);
1338
+ }
1339
+ }
1340
+ };
1341
+ }
1342
+ function backupRestoreCheck() {
1343
+ return {
1344
+ id: "backup-restore",
1345
+ description: "Delta DEEP CLONE backup restores a dropped scratch table",
1346
+ async run({ warehouse }) {
1347
+ const source = warehouse.qual(safeName("backup_source"));
1348
+ const backup = warehouse.qual(safeName("backup_clone"));
1349
+ try {
1350
+ await warehouse.exec(`CREATE TABLE ${source} USING DELTA AS SELECT 7 AS value`);
1351
+ await warehouse.exec(`CREATE TABLE ${backup} DEEP CLONE ${source}`);
1352
+ await warehouse.exec(`DROP TABLE ${source}`);
1353
+ await warehouse.exec(`CREATE TABLE ${source} DEEP CLONE ${backup}`);
1354
+ const rows = await warehouse.query(`SELECT value FROM ${source}`);
1355
+ if (Number(rows[0]?.value) !== 7) throw new Error("Restored Delta table lost its data");
1356
+ return { restoredRows: rows.length, deepClone: true };
1357
+ } finally {
1358
+ await warehouse.exec(`DROP TABLE IF EXISTS ${source}`).catch(() => void 0);
1359
+ await warehouse.exec(`DROP TABLE IF EXISTS ${backup}`).catch(() => void 0);
1360
+ }
1361
+ }
1362
+ };
1363
+ }
1364
+ function rollbackCheck() {
1365
+ return {
1366
+ id: "rollback",
1367
+ description: "Delta time-travel rollback restores a known-good table version",
1368
+ async run({ warehouse }) {
1369
+ const table = warehouse.qual(safeName("rollback_probe"));
1370
+ try {
1371
+ await warehouse.exec(`CREATE TABLE ${table} (value INT) USING DELTA`);
1372
+ await warehouse.exec(`INSERT INTO ${table} VALUES (1)`);
1373
+ const history = await warehouse.query(`DESCRIBE HISTORY ${table} LIMIT 1`);
1374
+ const knownGoodVersion = Number(history[0]?.version);
1375
+ if (!Number.isInteger(knownGoodVersion)) {
1376
+ throw new Error("Delta history did not return a known-good version");
1377
+ }
1378
+ await warehouse.exec(`INSERT INTO ${table} VALUES (999)`);
1379
+ await warehouse.exec(`RESTORE TABLE ${table} TO VERSION AS OF ${knownGoodVersion}`);
1380
+ const rows = await warehouse.query(`SELECT value FROM ${table} ORDER BY value`);
1381
+ if (rows.length !== 1 || Number(rows[0]?.value) !== 1) {
1382
+ throw new Error("Delta rollback did not restore the known-good contents");
1383
+ }
1384
+ return { knownGoodVersion, restoredRows: rows.length };
1385
+ } finally {
1386
+ await warehouse.exec(`DROP TABLE IF EXISTS ${table}`).catch(() => void 0);
1387
+ }
1388
+ }
1389
+ };
1390
+ }
1391
+ function serverlessComputeCheck() {
1392
+ return {
1393
+ id: "azure-serverless",
1394
+ description: "Azure Databricks serverless SQL warehouse is enabled and executes SQL",
1395
+ configured: (env) => Boolean(env.DATABRICKS_WAREHOUSE_ID),
1396
+ async run({ env, client, warehouse }) {
1397
+ const id = env.DATABRICKS_WAREHOUSE_ID;
1398
+ const details = await client.get(`/api/2.0/sql/warehouses/${encodeURIComponent(id)}`);
1399
+ if (details.enable_serverless_compute !== true) {
1400
+ throw new Error(`SQL warehouse ${id} is not configured for serverless compute`);
1401
+ }
1402
+ const rows = await warehouse.query("SELECT current_catalog() AS catalog");
1403
+ return { warehouseId: id, name: details.name, state: details.state, rows: rows.length };
1404
+ }
1405
+ };
1406
+ }
1407
+ function classicComputeCheck() {
1408
+ return {
1409
+ id: "azure-classic",
1410
+ description: "Azure Databricks classic job cluster starts and completes a notebook workload",
1411
+ configured: (env) => Boolean(
1412
+ env.DBX_TEST_CLASSIC_SPARK_VERSION && env.DBX_TEST_CLASSIC_NODE_TYPE && env.DBX_TEST_NOTEBOOK_PATH
1413
+ ),
1414
+ async run({ env, client }) {
1415
+ const submitted = await client.post("/api/2.1/jobs/runs/submit", {
1416
+ run_name: `fabric-experiments-classic-certification-${Date.now()}`,
1417
+ tasks: [
1418
+ {
1419
+ task_key: "classic_notebook",
1420
+ new_cluster: {
1421
+ spark_version: env.DBX_TEST_CLASSIC_SPARK_VERSION,
1422
+ node_type_id: env.DBX_TEST_CLASSIC_NODE_TYPE,
1423
+ num_workers: Number(env.DBX_TEST_CLASSIC_WORKERS ?? 1),
1424
+ autotermination_minutes: 10,
1425
+ data_security_mode: env.DBX_TEST_CLASSIC_SECURITY_MODE ?? "SINGLE_USER"
1426
+ },
1427
+ notebook_task: { notebook_path: env.DBX_TEST_NOTEBOOK_PATH }
1428
+ }
1429
+ ]
1430
+ });
1431
+ if (!submitted.run_id) throw new Error("Classic compute submit returned no run_id");
1432
+ const run = await databricks.waitForDatabricksRun(client, submitted.run_id, {
1433
+ timeoutMs: Number(env.DBX_TEST_CLASSIC_TIMEOUT_MS ?? 18e5),
1434
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 15e3)
1435
+ });
1436
+ const result = run.state?.result_state;
1437
+ if (result !== "SUCCESS") {
1438
+ throw new Error(`Classic compute run ended in ${result ?? "UNKNOWN"}`);
1439
+ }
1440
+ return {
1441
+ runId: submitted.run_id,
1442
+ sparkVersion: env.DBX_TEST_CLASSIC_SPARK_VERSION,
1443
+ nodeType: env.DBX_TEST_CLASSIC_NODE_TYPE
1444
+ };
1445
+ }
1446
+ };
1447
+ }
1259
1448
 
1260
1449
  Object.defineProperty(exports, "waitForDatabricksRun", {
1261
1450
  enumerable: true,
@@ -1268,6 +1457,8 @@ Object.defineProperty(exports, "waitForDatabricksStatement", {
1268
1457
  exports.appHealthCheck = appHealthCheck;
1269
1458
  exports.assertDeltaContract = assertDeltaContract;
1270
1459
  exports.autoLoaderIngestionCheck = autoLoaderIngestionCheck;
1460
+ exports.backupRestoreCheck = backupRestoreCheck;
1461
+ exports.classicComputeCheck = classicComputeCheck;
1271
1462
  exports.compareToGolden = compareToGolden;
1272
1463
  exports.createClientFromEnv = createClientFromEnv;
1273
1464
  exports.createDuckDbContext = createDuckDbContext;
@@ -1285,6 +1476,7 @@ exports.deltaContractCheck = deltaContractCheck;
1285
1476
  exports.ensureVolumeDirectory = ensureVolumeDirectory;
1286
1477
  exports.expectQueryToMatchGolden = expectQueryToMatchGolden;
1287
1478
  exports.expectTable = expectTable;
1479
+ exports.failureRecoveryCheck = failureRecoveryCheck;
1288
1480
  exports.jobRunCheck = jobRunCheck;
1289
1481
  exports.lakebaseCredentialCheck = lakebaseCredentialCheck;
1290
1482
  exports.lakebaseRoundTripCheck = lakebaseRoundTripCheck;
@@ -1293,6 +1485,7 @@ exports.normalizeVolumeRoot = normalizeVolumeRoot;
1293
1485
  exports.notebookSubmitCheck = notebookSubmitCheck;
1294
1486
  exports.parseFixtureColumns = parseFixtureColumns;
1295
1487
  exports.parseStatementRows = parseStatementRows;
1488
+ exports.performanceBudgetCheck = performanceBudgetCheck;
1296
1489
  exports.pipelineRefreshCheck = pipelineRefreshCheck;
1297
1490
  exports.renderJUnit = renderJUnit;
1298
1491
  exports.resolveFixtureRows = resolveFixtureRows;
@@ -1300,8 +1493,11 @@ exports.resolveLakebaseProject = resolveLakebaseProject;
1300
1493
  exports.resolveProfile = resolveProfile;
1301
1494
  exports.resolveTokenProvider = resolveTokenProvider;
1302
1495
  exports.restrictedPrincipalCheck = restrictedPrincipalCheck;
1496
+ exports.rollbackCheck = rollbackCheck;
1303
1497
  exports.runLiveSuite = runLiveSuite;
1498
+ exports.secretRotationCheck = secretRotationCheck;
1304
1499
  exports.secretScopeCheck = secretScopeCheck;
1500
+ exports.serverlessComputeCheck = serverlessComputeCheck;
1305
1501
  exports.sqlRoundTripCheck = sqlRoundTripCheck;
1306
1502
  exports.toLakebaseBranchId = toLakebaseBranchId;
1307
1503
  exports.toNamedParameters = toNamedParameters;