@fabricorg/databricks-testkit 0.1.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.d.cts CHANGED
@@ -170,7 +170,7 @@ declare function compareToGolden(goldenPath: string, actual: readonly Record<str
170
170
  * vitest without vi.fn so it is equally usable from cucumber-js worlds.
171
171
  */
172
172
  interface RecordedCall {
173
- method: 'GET' | 'POST';
173
+ method: 'GET' | 'POST' | 'DELETE';
174
174
  path: string;
175
175
  body?: unknown;
176
176
  query?: Record<string, string | number | boolean | undefined>;
@@ -178,12 +178,13 @@ interface RecordedCall {
178
178
  interface MockDatabricksClient {
179
179
  get<T>(path: string, query?: Record<string, string | number | boolean | undefined>): Promise<T>;
180
180
  post<T>(path: string, body?: unknown, query?: Record<string, string | number | boolean | undefined>, options?: unknown): Promise<T>;
181
+ request<T>(method: 'GET' | 'POST' | 'DELETE', path: string, body?: unknown, query?: Record<string, string | number | boolean | undefined>, options?: unknown): Promise<T>;
181
182
  /** Queue the next response for calls whose path starts with `pathPrefix`. FIFO per prefix. */
182
- queue(method: 'GET' | 'POST', pathPrefix: string, response: unknown): MockDatabricksClient;
183
+ queue(method: 'GET' | 'POST' | 'DELETE', pathPrefix: string, response: unknown): MockDatabricksClient;
183
184
  /** Queue an error for the next matching call. */
184
- queueError(method: 'GET' | 'POST', pathPrefix: string, error: Error): MockDatabricksClient;
185
+ queueError(method: 'GET' | 'POST' | 'DELETE', pathPrefix: string, error: Error): MockDatabricksClient;
185
186
  readonly calls: readonly RecordedCall[];
186
- callsTo(method: 'GET' | 'POST', pathPrefix: string): RecordedCall[];
187
+ callsTo(method: 'GET' | 'POST' | 'DELETE', pathPrefix: string): RecordedCall[];
187
188
  }
188
189
  declare function createMockDatabricksClient(): MockDatabricksClient;
189
190
 
@@ -239,6 +240,8 @@ declare function runLiveSuite(spec: LiveSuiteSpec, runtime?: LiveSuiteRuntime):
239
240
  declare function renderJUnit(evidence: LiveEvidence): string;
240
241
 
241
242
  declare function sqlRoundTripCheck(): LiveCheck;
243
+ /** Fail closed if governance checks accidentally run with bootstrap or administrator credentials. */
244
+ declare function restrictedPrincipalCheck(): LiveCheck;
242
245
  interface DeltaColumnContract {
243
246
  name: string;
244
247
  type?: string;
@@ -264,6 +267,37 @@ declare function lakebaseCredentialCheck(options?: LakebaseCheckOptions): LiveCh
264
267
  /** Exercise the production provider before and after an explicit credential/pool refresh. */
265
268
  declare function lakebaseRoundTripCheck(options?: LakebaseCheckOptions): LiveCheck;
266
269
 
270
+ interface LakebaseControlClient {
271
+ get<T>(path: string, query?: Record<string, string | number | boolean | undefined>): Promise<T>;
272
+ post<T>(path: string, body?: unknown, query?: Record<string, string | number | boolean | undefined>): Promise<T>;
273
+ request<T>(method: 'DELETE', path: string, body?: unknown, query?: Record<string, string | number | boolean | undefined>): Promise<T>;
274
+ }
275
+ interface EphemeralLakebaseBranch {
276
+ project: string;
277
+ branch: string;
278
+ branchId: string;
279
+ endpoint: string;
280
+ host: string;
281
+ databaseResource: string;
282
+ database: string;
283
+ createdAt: string;
284
+ expiresAt: string;
285
+ }
286
+ interface LakebaseBranchOptions {
287
+ env?: Record<string, string | undefined>;
288
+ client?: LakebaseControlClient;
289
+ project?: string;
290
+ branchId: string;
291
+ ttlMs: number;
292
+ pollIntervalMs?: number;
293
+ timeoutMs?: number;
294
+ now?: () => Date;
295
+ }
296
+ declare function resolveLakebaseProject(env: Record<string, string | undefined>, explicit?: string): string;
297
+ declare function toLakebaseBranchId(environmentName: string): string;
298
+ declare function createEphemeralLakebaseBranch(options: LakebaseBranchOptions): Promise<EphemeralLakebaseBranch>;
299
+ declare function deleteEphemeralLakebaseBranch(branch: Pick<EphemeralLakebaseBranch, 'branch'>, options?: Omit<LakebaseBranchOptions, 'branchId' | 'ttlMs' | 'project'>): Promise<void>;
300
+
267
301
  /** Assert secret key metadata only; values are never requested. */
268
302
  declare function secretScopeCheck(): LiveCheck;
269
303
  interface AppHealthCheckOptions {
@@ -298,4 +332,22 @@ interface PollOptions {
298
332
  }
299
333
  declare function waitForPipelineUpdate(client: WorkspaceClientLike, pipelineId: string, updateId: string, options?: PollOptions): Promise<Record<string, unknown>>;
300
334
 
301
- export { type AppHealthCheckOptions, type AutoLoaderCheckOptions, type CreateContextOptions, type DefinedLiveSuite, type DeltaColumnContract, type DuckDbConnectionLike, type DuckDbContextOptions, type ExecutionContext, type LakebaseCheckOptions, type LiveCheck, type LiveCheckContext, type LiveCheckResult, type LiveCheckStatus, type LiveEvidence, type LiveSuiteRuntime, type LiveSuiteSpec, type MockDatabricksClient, type PollOptions, type RecordedCall, TableFixture, type TestProfile, type VolumeIOCheckOptions, type WorkspaceClientLike, type WorkspaceContextOptions, appHealthCheck, assertDeltaContract, autoLoaderIngestionCheck, compareToGolden, createClientFromEnv, createDuckDbContext, createExecutionContext, createLakebaseTestProvider, createMockDatabricksClient, createWorkspaceContext, customLiveCheck, dbtBuildCheck, defineLiveSuite, deleteVolumeFile, deltaContractCheck, ensureVolumeDirectory, expectQueryToMatchGolden, expectTable, jobRunCheck, lakebaseCredentialCheck, lakebaseRoundTripCheck, normalizeRow, normalizeVolumeRoot, notebookSubmitCheck, parseStatementRows, pipelineRefreshCheck, renderJUnit, resolveProfile, resolveTokenProvider, runLiveSuite, secretScopeCheck, sqlRoundTripCheck, toNamedParameters, unityCatalogAccessCheck, uploadVolumeFile, volumeIOCheck, waitForPipelineUpdate };
335
+ /** Measure the real Statement Execution path and fail when its p95 exceeds the declared budget. */
336
+ declare function performanceBudgetCheck(): LiveCheck;
337
+ /** Prove a failed statement does not poison the session or the next workload. */
338
+ declare function failureRecoveryCheck(): LiveCheck;
339
+ /** Rotate a disposable secret key and remove it. Values are never returned or written to evidence. */
340
+ interface SecretRotationCheckOptions {
341
+ randomId?: () => string;
342
+ }
343
+ declare function secretRotationCheck(options?: SecretRotationCheckOptions): LiveCheck;
344
+ /** Exercise a complete Delta backup, destructive loss, and restore cycle in the scratch schema. */
345
+ declare function backupRestoreCheck(): LiveCheck;
346
+ /** Prove Delta time-travel rollback returns a corrupted scratch table to its known-good version. */
347
+ declare function rollbackCheck(): LiveCheck;
348
+ /** Certify that the configured SQL warehouse is serverless and usable by the test identity. */
349
+ declare function serverlessComputeCheck(): LiveCheck;
350
+ /** Launch a disposable classic job cluster and require the shared notebook probe to succeed. */
351
+ declare function classicComputeCheck(): LiveCheck;
352
+
353
+ export { type AppHealthCheckOptions, type AutoLoaderCheckOptions, type CreateContextOptions, type DefinedLiveSuite, type DeltaColumnContract, type DuckDbConnectionLike, type DuckDbContextOptions, type EphemeralLakebaseBranch, type ExecutionContext, type LakebaseBranchOptions, type LakebaseCheckOptions, type LakebaseControlClient, type LiveCheck, type LiveCheckContext, type LiveCheckResult, type LiveCheckStatus, type LiveEvidence, type LiveSuiteRuntime, type LiveSuiteSpec, type MockDatabricksClient, type PollOptions, type RecordedCall, type SecretRotationCheckOptions, TableFixture, type TestProfile, type VolumeIOCheckOptions, type WorkspaceClientLike, type WorkspaceContextOptions, appHealthCheck, assertDeltaContract, autoLoaderIngestionCheck, backupRestoreCheck, classicComputeCheck, compareToGolden, createClientFromEnv, createDuckDbContext, createEphemeralLakebaseBranch, createExecutionContext, createLakebaseTestProvider, createMockDatabricksClient, createWorkspaceContext, customLiveCheck, dbtBuildCheck, defineLiveSuite, deleteEphemeralLakebaseBranch, deleteVolumeFile, deltaContractCheck, ensureVolumeDirectory, expectQueryToMatchGolden, expectTable, failureRecoveryCheck, jobRunCheck, lakebaseCredentialCheck, lakebaseRoundTripCheck, normalizeRow, normalizeVolumeRoot, notebookSubmitCheck, parseStatementRows, performanceBudgetCheck, pipelineRefreshCheck, renderJUnit, resolveLakebaseProject, resolveProfile, resolveTokenProvider, restrictedPrincipalCheck, rollbackCheck, runLiveSuite, secretRotationCheck, secretScopeCheck, serverlessComputeCheck, sqlRoundTripCheck, toLakebaseBranchId, toNamedParameters, unityCatalogAccessCheck, uploadVolumeFile, volumeIOCheck, waitForPipelineUpdate };
package/dist/index.d.ts CHANGED
@@ -170,7 +170,7 @@ declare function compareToGolden(goldenPath: string, actual: readonly Record<str
170
170
  * vitest without vi.fn so it is equally usable from cucumber-js worlds.
171
171
  */
172
172
  interface RecordedCall {
173
- method: 'GET' | 'POST';
173
+ method: 'GET' | 'POST' | 'DELETE';
174
174
  path: string;
175
175
  body?: unknown;
176
176
  query?: Record<string, string | number | boolean | undefined>;
@@ -178,12 +178,13 @@ interface RecordedCall {
178
178
  interface MockDatabricksClient {
179
179
  get<T>(path: string, query?: Record<string, string | number | boolean | undefined>): Promise<T>;
180
180
  post<T>(path: string, body?: unknown, query?: Record<string, string | number | boolean | undefined>, options?: unknown): Promise<T>;
181
+ request<T>(method: 'GET' | 'POST' | 'DELETE', path: string, body?: unknown, query?: Record<string, string | number | boolean | undefined>, options?: unknown): Promise<T>;
181
182
  /** Queue the next response for calls whose path starts with `pathPrefix`. FIFO per prefix. */
182
- queue(method: 'GET' | 'POST', pathPrefix: string, response: unknown): MockDatabricksClient;
183
+ queue(method: 'GET' | 'POST' | 'DELETE', pathPrefix: string, response: unknown): MockDatabricksClient;
183
184
  /** Queue an error for the next matching call. */
184
- queueError(method: 'GET' | 'POST', pathPrefix: string, error: Error): MockDatabricksClient;
185
+ queueError(method: 'GET' | 'POST' | 'DELETE', pathPrefix: string, error: Error): MockDatabricksClient;
185
186
  readonly calls: readonly RecordedCall[];
186
- callsTo(method: 'GET' | 'POST', pathPrefix: string): RecordedCall[];
187
+ callsTo(method: 'GET' | 'POST' | 'DELETE', pathPrefix: string): RecordedCall[];
187
188
  }
188
189
  declare function createMockDatabricksClient(): MockDatabricksClient;
189
190
 
@@ -239,6 +240,8 @@ declare function runLiveSuite(spec: LiveSuiteSpec, runtime?: LiveSuiteRuntime):
239
240
  declare function renderJUnit(evidence: LiveEvidence): string;
240
241
 
241
242
  declare function sqlRoundTripCheck(): LiveCheck;
243
+ /** Fail closed if governance checks accidentally run with bootstrap or administrator credentials. */
244
+ declare function restrictedPrincipalCheck(): LiveCheck;
242
245
  interface DeltaColumnContract {
243
246
  name: string;
244
247
  type?: string;
@@ -264,6 +267,37 @@ declare function lakebaseCredentialCheck(options?: LakebaseCheckOptions): LiveCh
264
267
  /** Exercise the production provider before and after an explicit credential/pool refresh. */
265
268
  declare function lakebaseRoundTripCheck(options?: LakebaseCheckOptions): LiveCheck;
266
269
 
270
+ interface LakebaseControlClient {
271
+ get<T>(path: string, query?: Record<string, string | number | boolean | undefined>): Promise<T>;
272
+ post<T>(path: string, body?: unknown, query?: Record<string, string | number | boolean | undefined>): Promise<T>;
273
+ request<T>(method: 'DELETE', path: string, body?: unknown, query?: Record<string, string | number | boolean | undefined>): Promise<T>;
274
+ }
275
+ interface EphemeralLakebaseBranch {
276
+ project: string;
277
+ branch: string;
278
+ branchId: string;
279
+ endpoint: string;
280
+ host: string;
281
+ databaseResource: string;
282
+ database: string;
283
+ createdAt: string;
284
+ expiresAt: string;
285
+ }
286
+ interface LakebaseBranchOptions {
287
+ env?: Record<string, string | undefined>;
288
+ client?: LakebaseControlClient;
289
+ project?: string;
290
+ branchId: string;
291
+ ttlMs: number;
292
+ pollIntervalMs?: number;
293
+ timeoutMs?: number;
294
+ now?: () => Date;
295
+ }
296
+ declare function resolveLakebaseProject(env: Record<string, string | undefined>, explicit?: string): string;
297
+ declare function toLakebaseBranchId(environmentName: string): string;
298
+ declare function createEphemeralLakebaseBranch(options: LakebaseBranchOptions): Promise<EphemeralLakebaseBranch>;
299
+ declare function deleteEphemeralLakebaseBranch(branch: Pick<EphemeralLakebaseBranch, 'branch'>, options?: Omit<LakebaseBranchOptions, 'branchId' | 'ttlMs' | 'project'>): Promise<void>;
300
+
267
301
  /** Assert secret key metadata only; values are never requested. */
268
302
  declare function secretScopeCheck(): LiveCheck;
269
303
  interface AppHealthCheckOptions {
@@ -298,4 +332,22 @@ interface PollOptions {
298
332
  }
299
333
  declare function waitForPipelineUpdate(client: WorkspaceClientLike, pipelineId: string, updateId: string, options?: PollOptions): Promise<Record<string, unknown>>;
300
334
 
301
- export { type AppHealthCheckOptions, type AutoLoaderCheckOptions, type CreateContextOptions, type DefinedLiveSuite, type DeltaColumnContract, type DuckDbConnectionLike, type DuckDbContextOptions, type ExecutionContext, type LakebaseCheckOptions, type LiveCheck, type LiveCheckContext, type LiveCheckResult, type LiveCheckStatus, type LiveEvidence, type LiveSuiteRuntime, type LiveSuiteSpec, type MockDatabricksClient, type PollOptions, type RecordedCall, TableFixture, type TestProfile, type VolumeIOCheckOptions, type WorkspaceClientLike, type WorkspaceContextOptions, appHealthCheck, assertDeltaContract, autoLoaderIngestionCheck, compareToGolden, createClientFromEnv, createDuckDbContext, createExecutionContext, createLakebaseTestProvider, createMockDatabricksClient, createWorkspaceContext, customLiveCheck, dbtBuildCheck, defineLiveSuite, deleteVolumeFile, deltaContractCheck, ensureVolumeDirectory, expectQueryToMatchGolden, expectTable, jobRunCheck, lakebaseCredentialCheck, lakebaseRoundTripCheck, normalizeRow, normalizeVolumeRoot, notebookSubmitCheck, parseStatementRows, pipelineRefreshCheck, renderJUnit, resolveProfile, resolveTokenProvider, runLiveSuite, secretScopeCheck, sqlRoundTripCheck, toNamedParameters, unityCatalogAccessCheck, uploadVolumeFile, volumeIOCheck, waitForPipelineUpdate };
335
+ /** Measure the real Statement Execution path and fail when its p95 exceeds the declared budget. */
336
+ declare function performanceBudgetCheck(): LiveCheck;
337
+ /** Prove a failed statement does not poison the session or the next workload. */
338
+ declare function failureRecoveryCheck(): LiveCheck;
339
+ /** Rotate a disposable secret key and remove it. Values are never returned or written to evidence. */
340
+ interface SecretRotationCheckOptions {
341
+ randomId?: () => string;
342
+ }
343
+ declare function secretRotationCheck(options?: SecretRotationCheckOptions): LiveCheck;
344
+ /** Exercise a complete Delta backup, destructive loss, and restore cycle in the scratch schema. */
345
+ declare function backupRestoreCheck(): LiveCheck;
346
+ /** Prove Delta time-travel rollback returns a corrupted scratch table to its known-good version. */
347
+ declare function rollbackCheck(): LiveCheck;
348
+ /** Certify that the configured SQL warehouse is serverless and usable by the test identity. */
349
+ declare function serverlessComputeCheck(): LiveCheck;
350
+ /** Launch a disposable classic job cluster and require the shared notebook probe to succeed. */
351
+ declare function classicComputeCheck(): LiveCheck;
352
+
353
+ export { type AppHealthCheckOptions, type AutoLoaderCheckOptions, type CreateContextOptions, type DefinedLiveSuite, type DeltaColumnContract, type DuckDbConnectionLike, type DuckDbContextOptions, type EphemeralLakebaseBranch, type ExecutionContext, type LakebaseBranchOptions, type LakebaseCheckOptions, type LakebaseControlClient, type LiveCheck, type LiveCheckContext, type LiveCheckResult, type LiveCheckStatus, type LiveEvidence, type LiveSuiteRuntime, type LiveSuiteSpec, type MockDatabricksClient, type PollOptions, type RecordedCall, type SecretRotationCheckOptions, TableFixture, type TestProfile, type VolumeIOCheckOptions, type WorkspaceClientLike, type WorkspaceContextOptions, appHealthCheck, assertDeltaContract, autoLoaderIngestionCheck, backupRestoreCheck, classicComputeCheck, compareToGolden, createClientFromEnv, createDuckDbContext, createEphemeralLakebaseBranch, createExecutionContext, createLakebaseTestProvider, createMockDatabricksClient, createWorkspaceContext, customLiveCheck, dbtBuildCheck, defineLiveSuite, deleteEphemeralLakebaseBranch, deleteVolumeFile, deltaContractCheck, ensureVolumeDirectory, expectQueryToMatchGolden, expectTable, failureRecoveryCheck, jobRunCheck, lakebaseCredentialCheck, lakebaseRoundTripCheck, normalizeRow, normalizeVolumeRoot, notebookSubmitCheck, parseStatementRows, performanceBudgetCheck, pipelineRefreshCheck, renderJUnit, resolveLakebaseProject, resolveProfile, resolveTokenProvider, restrictedPrincipalCheck, rollbackCheck, runLiveSuite, secretRotationCheck, secretScopeCheck, serverlessComputeCheck, sqlRoundTripCheck, toLakebaseBranchId, toNamedParameters, unityCatalogAccessCheck, uploadVolumeFile, volumeIOCheck, waitForPipelineUpdate };
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { dirname, resolve, basename } from 'path';
9
9
  import { waitForDatabricksRun } from '@fabric-harness/databricks';
10
10
  export { waitForDatabricksRun, waitForDatabricksStatement } from '@fabric-harness/databricks';
11
11
  import { LakebaseDatabaseProvider, exchangeLakebaseCredential } from '@fabricorg/experiments-db-pool';
12
- import { randomUUID } from 'crypto';
12
+ import { randomUUID, randomBytes } from 'crypto';
13
13
 
14
14
  // src/context.ts
15
15
  function resolveProfile(env = process.env) {
@@ -91,7 +91,7 @@ function looselyEqual(a, b) {
91
91
 
92
92
  // src/mock-client.ts
93
93
  function createMockDatabricksClient() {
94
- const queues = { GET: [], POST: [] };
94
+ const queues = { GET: [], POST: [], DELETE: [] };
95
95
  const calls = [];
96
96
  function take(method, path) {
97
97
  const index = queues[method].findIndex((q) => path.startsWith(q.pathPrefix));
@@ -115,6 +115,12 @@ function createMockDatabricksClient() {
115
115
  if (queued.error) throw queued.error;
116
116
  return queued.response;
117
117
  },
118
+ async request(method, path, body, query) {
119
+ calls.push({ method, path, body, query });
120
+ const queued = take(method, path);
121
+ if (queued.error) throw queued.error;
122
+ return queued.response;
123
+ },
118
124
  queue(method, pathPrefix, response) {
119
125
  queues[method].push({ pathPrefix, response });
120
126
  return client;
@@ -280,6 +286,26 @@ function sqlRoundTripCheck() {
280
286
  }
281
287
  };
282
288
  }
289
+ function restrictedPrincipalCheck() {
290
+ return {
291
+ id: "restricted-principal",
292
+ description: "Live checks use the designated least-privilege service principal",
293
+ configured: (env) => Boolean(env.DBX_TEST_RESTRICTED_PRINCIPAL_ID),
294
+ async run({ env }) {
295
+ const expected = env.DBX_TEST_RESTRICTED_PRINCIPAL_ID?.toLowerCase();
296
+ const actual = env.DATABRICKS_CLIENT_ID?.toLowerCase();
297
+ if (!expected || !actual) {
298
+ throw new Error(
299
+ "Restricted-principal check requires DBX_TEST_RESTRICTED_PRINCIPAL_ID and DATABRICKS_CLIENT_ID"
300
+ );
301
+ }
302
+ if (actual !== expected) {
303
+ throw new Error(`Live suite is using principal '${actual}', expected '${expected}'`);
304
+ }
305
+ return { principalId: actual };
306
+ }
307
+ };
308
+ }
283
309
  function assertDeltaContract(table, rows, columns) {
284
310
  for (const expected of columns) {
285
311
  const actual = rows.find((row) => String(row.col_name ?? row.column_name) === expected.name);
@@ -371,17 +397,21 @@ function notebookSubmitCheck() {
371
397
  return {
372
398
  id: "notebook",
373
399
  description: "Databricks notebook submit run reaches SUCCESS",
374
- configured: (env) => Boolean(env.DBX_TEST_NOTEBOOK_PATH && env.DBX_TEST_CLUSTER_ID),
400
+ configured: (env) => Boolean(
401
+ env.DBX_TEST_NOTEBOOK_PATH && (env.DBX_TEST_CLUSTER_ID || env.DBX_TEST_NOTEBOOK_SERVERLESS === "1")
402
+ ),
375
403
  async run({ env, client }) {
376
- if (!env.DBX_TEST_NOTEBOOK_PATH || !env.DBX_TEST_CLUSTER_ID) {
377
- throw new Error("Notebook submit requires DBX_TEST_NOTEBOOK_PATH and DBX_TEST_CLUSTER_ID");
404
+ if (!env.DBX_TEST_NOTEBOOK_PATH || !env.DBX_TEST_CLUSTER_ID && env.DBX_TEST_NOTEBOOK_SERVERLESS !== "1") {
405
+ throw new Error(
406
+ "Notebook submit requires DBX_TEST_NOTEBOOK_PATH and either DBX_TEST_CLUSTER_ID or DBX_TEST_NOTEBOOK_SERVERLESS=1"
407
+ );
378
408
  }
379
409
  const submitted = await client.post("/api/2.1/jobs/runs/submit", {
380
410
  run_name: `fabric-experiments-notebook-${Date.now()}`,
381
411
  tasks: [
382
412
  {
383
413
  task_key: "notebook",
384
- existing_cluster_id: env.DBX_TEST_CLUSTER_ID,
414
+ ...env.DBX_TEST_CLUSTER_ID ? { existing_cluster_id: env.DBX_TEST_CLUSTER_ID } : {},
385
415
  notebook_task: { notebook_path: env.DBX_TEST_NOTEBOOK_PATH }
386
416
  }
387
417
  ]
@@ -401,29 +431,59 @@ function dbtBuildCheck() {
401
431
  return {
402
432
  id: "dbt",
403
433
  description: "Databricks dbt task reaches SUCCESS",
404
- configured: (env) => Boolean(env.DBX_TEST_DBT_GIT_URL && env.DATABRICKS_WAREHOUSE_ID),
434
+ configured: (env) => Boolean(
435
+ (env.DBX_TEST_DBT_GIT_URL || env.DBX_TEST_DBT_PROJECT_DIR) && env.DATABRICKS_WAREHOUSE_ID
436
+ ),
405
437
  async run({ env, client }) {
406
- if (!env.DBX_TEST_DBT_GIT_URL || !env.DATABRICKS_WAREHOUSE_ID) {
407
- throw new Error("dbt build requires DBX_TEST_DBT_GIT_URL and DATABRICKS_WAREHOUSE_ID");
438
+ if (!env.DBX_TEST_DBT_GIT_URL && !env.DBX_TEST_DBT_PROJECT_DIR || !env.DATABRICKS_WAREHOUSE_ID) {
439
+ throw new Error(
440
+ "dbt build requires DBX_TEST_DBT_GIT_URL or DBX_TEST_DBT_PROJECT_DIR, plus DATABRICKS_WAREHOUSE_ID"
441
+ );
408
442
  }
409
- const submitted = await client.post("/api/2.1/jobs/runs/submit", {
443
+ const sourceVars = JSON.stringify({
444
+ source_catalog: env.DBX_TEST_DBT_SOURCE_CATALOG ?? env.DBX_TEST_CATALOG,
445
+ source_schema: env.DBX_TEST_DBT_SOURCE_SCHEMA
446
+ });
447
+ const serverless = env.DBX_TEST_DBT_SERVERLESS === "1";
448
+ const request = {
410
449
  run_name: `fabric-experiments-dbt-${Date.now()}`,
411
- git_source: {
412
- git_url: env.DBX_TEST_DBT_GIT_URL,
413
- git_provider: env.DBX_TEST_DBT_GIT_PROVIDER ?? "gitHub",
414
- git_branch: env.DBX_TEST_DBT_GIT_BRANCH ?? "main"
415
- },
450
+ ...env.DBX_TEST_DBT_GIT_URL ? {
451
+ git_source: {
452
+ git_url: env.DBX_TEST_DBT_GIT_URL,
453
+ git_provider: env.DBX_TEST_DBT_GIT_PROVIDER ?? "gitHub",
454
+ git_branch: env.DBX_TEST_DBT_GIT_BRANCH ?? "main"
455
+ }
456
+ } : {},
416
457
  tasks: [
417
458
  {
418
459
  task_key: "dbt_build",
460
+ ...serverless ? { environment_key: "dbt" } : {},
419
461
  dbt_task: {
420
- commands: ["dbt deps", "dbt build"],
462
+ commands: ["dbt deps", `dbt build --vars '${sourceVars}'`],
421
463
  warehouse_id: env.DATABRICKS_WAREHOUSE_ID,
464
+ ...env.DBX_TEST_CATALOG ? { catalog: env.DBX_TEST_CATALOG } : {},
465
+ ...env.DBX_TEST_SCHEMA ? { schema: env.DBX_TEST_SCHEMA } : {},
466
+ ...env.DBX_TEST_DBT_GIT_URL ? {} : { source: "WORKSPACE" },
422
467
  ...env.DBX_TEST_DBT_PROJECT_DIR ? { project_directory: env.DBX_TEST_DBT_PROJECT_DIR } : {}
423
468
  }
424
469
  }
425
- ]
426
- });
470
+ ],
471
+ ...serverless ? {
472
+ environments: [
473
+ {
474
+ environment_key: "dbt",
475
+ spec: {
476
+ environment_version: "2",
477
+ dependencies: ["dbt-databricks>=1.8,<2"]
478
+ }
479
+ }
480
+ ]
481
+ } : {}
482
+ };
483
+ const submitted = await client.post(
484
+ "/api/2.1/jobs/runs/submit",
485
+ request
486
+ );
427
487
  if (!submitted.run_id) throw new Error("dbt submit returned no run_id");
428
488
  const run = await waitForDatabricksRun(client, submitted.run_id, {
429
489
  timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
@@ -431,7 +491,10 @@ function dbtBuildCheck() {
431
491
  });
432
492
  const result = run.state?.result_state;
433
493
  if (result !== "SUCCESS") throw new Error(`dbt run ended in ${result ?? "UNKNOWN"}`);
434
- return { runId: submitted.run_id, gitUrl: env.DBX_TEST_DBT_GIT_URL };
494
+ return {
495
+ runId: submitted.run_id,
496
+ source: env.DBX_TEST_DBT_GIT_URL ?? env.DBX_TEST_DBT_PROJECT_DIR
497
+ };
435
498
  }
436
499
  };
437
500
  }
@@ -507,6 +570,98 @@ function lakebaseRoundTripCheck(options = {}) {
507
570
  };
508
571
  }
509
572
 
573
+ // src/lakebase-branch.ts
574
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
575
+ var DEFAULT_TIMEOUT_MS = 5 * 6e4;
576
+ function resolveLakebaseProject(env, explicit) {
577
+ const project = explicit ?? env.DBX_TEST_LAKEBASE_PROJECT;
578
+ if (!project || !/^projects\/[a-z0-9][a-z0-9-]{0,62}$/.test(project)) {
579
+ throw new Error(
580
+ `Lakebase project must look like projects/fabric-experiments; got '${project ?? ""}'`
581
+ );
582
+ }
583
+ return project;
584
+ }
585
+ function toLakebaseBranchId(environmentName) {
586
+ const normalized = environmentName.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
587
+ const prefixed = normalized.startsWith("fx-") ? normalized : `fx-${normalized}`;
588
+ const trimmed = prefixed.slice(0, 63).replace(/-+$/g, "");
589
+ if (!/^fx-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(trimmed)) {
590
+ throw new Error(`Cannot derive a safe Lakebase branch ID from '${environmentName}'`);
591
+ }
592
+ return trimmed;
593
+ }
594
+ async function createEphemeralLakebaseBranch(options) {
595
+ const env = options.env ?? process.env;
596
+ const client = options.client ?? createClientFromEnv(env);
597
+ const project = resolveLakebaseProject(env, options.project);
598
+ const branchId = toLakebaseBranchId(options.branchId);
599
+ const ttlSeconds = Math.max(60, Math.ceil(options.ttlMs / 1e3));
600
+ const operation = await client.post(
601
+ `/api/2.0/postgres/${project}/branches`,
602
+ { spec: { ttl: `${ttlSeconds}s` } },
603
+ { branch_id: branchId, replace_existing: false }
604
+ );
605
+ const branch = await waitForOperation(client, operation, options);
606
+ const branchName = branch?.name ?? `${project}/branches/${branchId}`;
607
+ const [endpointResult, databaseResult] = await Promise.all([
608
+ client.get(`/api/2.0/postgres/${branchName}/endpoints`),
609
+ client.get(`/api/2.0/postgres/${branchName}/databases`)
610
+ ]);
611
+ const endpoint = endpointResult.endpoints?.find((item) => item.endpoint_id === "primary") ?? endpointResult.endpoints?.[0];
612
+ const database = databaseResult.databases?.[0];
613
+ const host = endpoint?.status?.hosts?.host;
614
+ const postgresDatabase = database?.status?.postgres_database;
615
+ if (!endpoint?.name || !host || !database?.name || !postgresDatabase) {
616
+ throw new Error(`Lakebase branch '${branchName}' did not expose an endpoint and database`);
617
+ }
618
+ const createdAt = branch?.create_time ?? (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
619
+ return {
620
+ project,
621
+ branch: branchName,
622
+ branchId,
623
+ endpoint: endpoint.name,
624
+ host,
625
+ databaseResource: database.name,
626
+ database: postgresDatabase,
627
+ createdAt,
628
+ expiresAt: new Date(Date.parse(createdAt) + ttlSeconds * 1e3).toISOString()
629
+ };
630
+ }
631
+ async function deleteEphemeralLakebaseBranch(branch, options = {}) {
632
+ if (!/^projects\/[a-z0-9-]+\/branches\/fx-[a-z0-9-]+$/.test(branch.branch)) {
633
+ throw new Error(`Refusing to delete non-ephemeral Lakebase branch '${branch.branch}'`);
634
+ }
635
+ const env = options.env ?? process.env;
636
+ const client = options.client ?? createClientFromEnv(env);
637
+ const operation = await client.request(
638
+ "DELETE",
639
+ `/api/2.0/postgres/${branch.branch}`,
640
+ void 0,
641
+ { purge: true }
642
+ );
643
+ await waitForOperation(client, operation, options);
644
+ }
645
+ async function waitForOperation(client, initial, options) {
646
+ let operation = initial;
647
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
648
+ while (!operation.done) {
649
+ if (!operation.name) throw new Error("Lakebase operation did not return an operation name");
650
+ if (Date.now() >= deadline) throw new Error(`Lakebase operation '${operation.name}' timed out`);
651
+ await delay(options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
652
+ operation = await client.get(`/api/2.0/postgres/${operation.name}`);
653
+ }
654
+ if (operation.error) {
655
+ throw new Error(
656
+ `Lakebase operation failed${operation.error.code ? ` (${operation.error.code})` : ""}: ${operation.error.message ?? "unknown error"}`
657
+ );
658
+ }
659
+ return operation.response;
660
+ }
661
+ function delay(ms) {
662
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
663
+ }
664
+
510
665
  // src/databricks-http.ts
511
666
  async function databricksFetch(env, pathOrUrl, init = {}, fetchImpl = fetch) {
512
667
  const host = env.DATABRICKS_HOST?.replace(/\/$/, "");
@@ -684,7 +839,196 @@ function autoLoaderIngestionCheck(options = {}) {
684
839
  }
685
840
  };
686
841
  }
842
+ function safeName(prefix) {
843
+ return `${prefix}_${randomUUID().replaceAll("-", "_")}`;
844
+ }
845
+ function performanceBudgetCheck() {
846
+ return {
847
+ id: "performance",
848
+ description: "SQL Statement Execution p95 stays within the configured latency budget",
849
+ configured: (env) => Boolean(env.DBX_TEST_PERFORMANCE_P95_MS),
850
+ async run({ env, warehouse }) {
851
+ const budgetMs = Number(env.DBX_TEST_PERFORMANCE_P95_MS);
852
+ const runs = Number(env.DBX_TEST_PERFORMANCE_RUNS ?? 7);
853
+ if (!Number.isFinite(budgetMs) || budgetMs <= 0) {
854
+ throw new Error("DBX_TEST_PERFORMANCE_P95_MS must be a positive number");
855
+ }
856
+ if (!Number.isInteger(runs) || runs < 3 || runs > 100) {
857
+ throw new Error("DBX_TEST_PERFORMANCE_RUNS must be an integer from 3 to 100");
858
+ }
859
+ await warehouse.query("SELECT 1 AS warmup");
860
+ const durations = [];
861
+ for (let index = 0; index < runs; index += 1) {
862
+ const started = performance.now();
863
+ await warehouse.query("SELECT 1 AS performance_probe");
864
+ durations.push(performance.now() - started);
865
+ }
866
+ durations.sort((left, right) => left - right);
867
+ const p95 = durations[Math.ceil(durations.length * 0.95) - 1] ?? 0;
868
+ if (p95 > budgetMs) {
869
+ throw new Error(`SQL p95 ${Math.round(p95)}ms exceeded ${budgetMs}ms budget`);
870
+ }
871
+ return {
872
+ runs,
873
+ p50Ms: Math.round(durations[Math.floor(durations.length * 0.5)] ?? 0),
874
+ p95Ms: Math.round(p95),
875
+ budgetMs
876
+ };
877
+ }
878
+ };
879
+ }
880
+ function failureRecoveryCheck() {
881
+ return {
882
+ id: "failure-recovery",
883
+ description: "A failed statement is isolated and a subsequent statement recovers",
884
+ async run({ warehouse }) {
885
+ let failedAsExpected = false;
886
+ try {
887
+ await warehouse.query("SELECT * FROM __fabric_experiments_expected_missing_table__");
888
+ } catch {
889
+ failedAsExpected = true;
890
+ }
891
+ if (!failedAsExpected) throw new Error("Injected failure unexpectedly succeeded");
892
+ const rows = await warehouse.query("SELECT 1 AS recovered");
893
+ if (Number(rows[0]?.recovered) !== 1) {
894
+ throw new Error("Statement execution did not recover after the injected failure");
895
+ }
896
+ return { injectedFailure: true, recovered: true };
897
+ }
898
+ };
899
+ }
900
+ function secretRotationCheck(options = {}) {
901
+ return {
902
+ id: "secret-rotation",
903
+ description: "Databricks secret put, overwrite, metadata verification, and cleanup",
904
+ configured: (env) => Boolean(env.DBX_TEST_ROTATION_SECRET_SCOPE),
905
+ async run({ env, client }) {
906
+ const scope = env.DBX_TEST_ROTATION_SECRET_SCOPE;
907
+ const key = options.randomId?.() ?? safeName("fx_rotation_probe");
908
+ const put = async (value) => client.post("/api/2.0/secrets/put", { scope, key, string_value: value });
909
+ try {
910
+ await put(randomBytes(32).toString("base64url"));
911
+ await put(randomBytes(32).toString("base64url"));
912
+ const listed = await client.get("/api/2.0/secrets/list", {
913
+ scope
914
+ });
915
+ if (!(listed.secrets ?? []).some((secret) => secret.key === key)) {
916
+ throw new Error(`Rotated secret metadata was not visible in scope ${scope}`);
917
+ }
918
+ return { scope, rotated: true, cleanedUp: true };
919
+ } finally {
920
+ await client.post("/api/2.0/secrets/delete", { scope, key }).catch(() => void 0);
921
+ }
922
+ }
923
+ };
924
+ }
925
+ function backupRestoreCheck() {
926
+ return {
927
+ id: "backup-restore",
928
+ description: "Delta DEEP CLONE backup restores a dropped scratch table",
929
+ async run({ warehouse }) {
930
+ const source = warehouse.qual(safeName("backup_source"));
931
+ const backup = warehouse.qual(safeName("backup_clone"));
932
+ try {
933
+ await warehouse.exec(`CREATE TABLE ${source} USING DELTA AS SELECT 7 AS value`);
934
+ await warehouse.exec(`CREATE TABLE ${backup} DEEP CLONE ${source}`);
935
+ await warehouse.exec(`DROP TABLE ${source}`);
936
+ await warehouse.exec(`CREATE TABLE ${source} DEEP CLONE ${backup}`);
937
+ const rows = await warehouse.query(`SELECT value FROM ${source}`);
938
+ if (Number(rows[0]?.value) !== 7) throw new Error("Restored Delta table lost its data");
939
+ return { restoredRows: rows.length, deepClone: true };
940
+ } finally {
941
+ await warehouse.exec(`DROP TABLE IF EXISTS ${source}`).catch(() => void 0);
942
+ await warehouse.exec(`DROP TABLE IF EXISTS ${backup}`).catch(() => void 0);
943
+ }
944
+ }
945
+ };
946
+ }
947
+ function rollbackCheck() {
948
+ return {
949
+ id: "rollback",
950
+ description: "Delta time-travel rollback restores a known-good table version",
951
+ async run({ warehouse }) {
952
+ const table = warehouse.qual(safeName("rollback_probe"));
953
+ try {
954
+ await warehouse.exec(`CREATE TABLE ${table} (value INT) USING DELTA`);
955
+ await warehouse.exec(`INSERT INTO ${table} VALUES (1)`);
956
+ const history = await warehouse.query(`DESCRIBE HISTORY ${table} LIMIT 1`);
957
+ const knownGoodVersion = Number(history[0]?.version);
958
+ if (!Number.isInteger(knownGoodVersion)) {
959
+ throw new Error("Delta history did not return a known-good version");
960
+ }
961
+ await warehouse.exec(`INSERT INTO ${table} VALUES (999)`);
962
+ await warehouse.exec(`RESTORE TABLE ${table} TO VERSION AS OF ${knownGoodVersion}`);
963
+ const rows = await warehouse.query(`SELECT value FROM ${table} ORDER BY value`);
964
+ if (rows.length !== 1 || Number(rows[0]?.value) !== 1) {
965
+ throw new Error("Delta rollback did not restore the known-good contents");
966
+ }
967
+ return { knownGoodVersion, restoredRows: rows.length };
968
+ } finally {
969
+ await warehouse.exec(`DROP TABLE IF EXISTS ${table}`).catch(() => void 0);
970
+ }
971
+ }
972
+ };
973
+ }
974
+ function serverlessComputeCheck() {
975
+ return {
976
+ id: "azure-serverless",
977
+ description: "Azure Databricks serverless SQL warehouse is enabled and executes SQL",
978
+ configured: (env) => Boolean(env.DATABRICKS_WAREHOUSE_ID),
979
+ async run({ env, client, warehouse }) {
980
+ const id = env.DATABRICKS_WAREHOUSE_ID;
981
+ const details = await client.get(`/api/2.0/sql/warehouses/${encodeURIComponent(id)}`);
982
+ if (details.enable_serverless_compute !== true) {
983
+ throw new Error(`SQL warehouse ${id} is not configured for serverless compute`);
984
+ }
985
+ const rows = await warehouse.query("SELECT current_catalog() AS catalog");
986
+ return { warehouseId: id, name: details.name, state: details.state, rows: rows.length };
987
+ }
988
+ };
989
+ }
990
+ function classicComputeCheck() {
991
+ return {
992
+ id: "azure-classic",
993
+ description: "Azure Databricks classic job cluster starts and completes a notebook workload",
994
+ configured: (env) => Boolean(
995
+ env.DBX_TEST_CLASSIC_SPARK_VERSION && env.DBX_TEST_CLASSIC_NODE_TYPE && env.DBX_TEST_NOTEBOOK_PATH
996
+ ),
997
+ async run({ env, client }) {
998
+ const submitted = await client.post("/api/2.1/jobs/runs/submit", {
999
+ run_name: `fabric-experiments-classic-certification-${Date.now()}`,
1000
+ tasks: [
1001
+ {
1002
+ task_key: "classic_notebook",
1003
+ new_cluster: {
1004
+ spark_version: env.DBX_TEST_CLASSIC_SPARK_VERSION,
1005
+ node_type_id: env.DBX_TEST_CLASSIC_NODE_TYPE,
1006
+ num_workers: Number(env.DBX_TEST_CLASSIC_WORKERS ?? 1),
1007
+ autotermination_minutes: 10,
1008
+ data_security_mode: env.DBX_TEST_CLASSIC_SECURITY_MODE ?? "SINGLE_USER"
1009
+ },
1010
+ notebook_task: { notebook_path: env.DBX_TEST_NOTEBOOK_PATH }
1011
+ }
1012
+ ]
1013
+ });
1014
+ if (!submitted.run_id) throw new Error("Classic compute submit returned no run_id");
1015
+ const run = await waitForDatabricksRun(client, submitted.run_id, {
1016
+ timeoutMs: Number(env.DBX_TEST_CLASSIC_TIMEOUT_MS ?? 18e5),
1017
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 15e3)
1018
+ });
1019
+ const result = run.state?.result_state;
1020
+ if (result !== "SUCCESS") {
1021
+ throw new Error(`Classic compute run ended in ${result ?? "UNKNOWN"}`);
1022
+ }
1023
+ return {
1024
+ runId: submitted.run_id,
1025
+ sparkVersion: env.DBX_TEST_CLASSIC_SPARK_VERSION,
1026
+ nodeType: env.DBX_TEST_CLASSIC_NODE_TYPE
1027
+ };
1028
+ }
1029
+ };
1030
+ }
687
1031
 
688
- export { appHealthCheck, assertDeltaContract, autoLoaderIngestionCheck, createExecutionContext, createLakebaseTestProvider, createMockDatabricksClient, customLiveCheck, dbtBuildCheck, defineLiveSuite, deleteVolumeFile, deltaContractCheck, ensureVolumeDirectory, expectQueryToMatchGolden, expectTable, jobRunCheck, lakebaseCredentialCheck, lakebaseRoundTripCheck, normalizeVolumeRoot, notebookSubmitCheck, pipelineRefreshCheck, renderJUnit, resolveProfile, runLiveSuite, secretScopeCheck, sqlRoundTripCheck, unityCatalogAccessCheck, uploadVolumeFile, volumeIOCheck, waitForPipelineUpdate };
1032
+ export { appHealthCheck, assertDeltaContract, autoLoaderIngestionCheck, backupRestoreCheck, classicComputeCheck, createEphemeralLakebaseBranch, createExecutionContext, createLakebaseTestProvider, createMockDatabricksClient, customLiveCheck, dbtBuildCheck, defineLiveSuite, deleteEphemeralLakebaseBranch, deleteVolumeFile, deltaContractCheck, ensureVolumeDirectory, expectQueryToMatchGolden, expectTable, failureRecoveryCheck, jobRunCheck, lakebaseCredentialCheck, lakebaseRoundTripCheck, normalizeVolumeRoot, notebookSubmitCheck, performanceBudgetCheck, pipelineRefreshCheck, renderJUnit, resolveLakebaseProject, resolveProfile, restrictedPrincipalCheck, rollbackCheck, runLiveSuite, secretRotationCheck, secretScopeCheck, serverlessComputeCheck, sqlRoundTripCheck, toLakebaseBranchId, unityCatalogAccessCheck, uploadVolumeFile, volumeIOCheck, waitForPipelineUpdate };
689
1033
  //# sourceMappingURL=index.js.map
690
1034
  //# sourceMappingURL=index.js.map