@fabricorg/databricks-bdd 0.3.2 → 0.5.0

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.
Files changed (41) hide show
  1. package/README.md +49 -3
  2. package/dist/actions-CuXXKUqe.d.ts +92 -0
  3. package/dist/actions-CxfxhBZh.d.cts +92 -0
  4. package/dist/actions.cjs +14 -0
  5. package/dist/actions.cjs.map +1 -0
  6. package/dist/actions.d.cts +6 -0
  7. package/dist/actions.d.ts +6 -0
  8. package/dist/actions.js +3 -0
  9. package/dist/actions.js.map +1 -0
  10. package/dist/cardinality.cjs +24 -0
  11. package/dist/cardinality.cjs.map +1 -0
  12. package/dist/cardinality.d.cts +19 -0
  13. package/dist/cardinality.d.ts +19 -0
  14. package/dist/cardinality.js +3 -0
  15. package/dist/cardinality.js.map +1 -0
  16. package/dist/{chunk-355WDWP3.js → chunk-3XDMBHFP.js} +101 -3
  17. package/dist/chunk-3XDMBHFP.js.map +1 -0
  18. package/dist/chunk-6JC65RH2.js +11 -0
  19. package/dist/chunk-6JC65RH2.js.map +1 -0
  20. package/dist/chunk-HTKRJLVX.js +70 -0
  21. package/dist/chunk-HTKRJLVX.js.map +1 -0
  22. package/dist/chunk-K5LJ7WSW.js +22 -0
  23. package/dist/chunk-K5LJ7WSW.js.map +1 -0
  24. package/dist/index.cjs +199 -0
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +24 -65
  27. package/dist/index.d.ts +24 -65
  28. package/dist/index.js +4 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/scoped-state.cjs +74 -0
  31. package/dist/scoped-state.cjs.map +1 -0
  32. package/dist/scoped-state.d.cts +30 -0
  33. package/dist/scoped-state.d.ts +30 -0
  34. package/dist/scoped-state.js +3 -0
  35. package/dist/scoped-state.js.map +1 -0
  36. package/dist/steps.cjs +239 -3
  37. package/dist/steps.cjs.map +1 -1
  38. package/dist/steps.js +73 -5
  39. package/dist/steps.js.map +1 -1
  40. package/package.json +32 -2
  41. package/dist/chunk-355WDWP3.js.map +0 -1
@@ -0,0 +1,30 @@
1
+ type StateScope = 'run' | 'feature' | 'scenario';
2
+ type StateValues = Map<string, unknown>;
3
+ /**
4
+ * Process-local state backing Behave-style run and feature context layers.
5
+ * Scenario values stay on the scenario World and are never shared.
6
+ */
7
+ declare class SharedStateRegistry {
8
+ private readonly runValues;
9
+ private readonly featureValues;
10
+ run(): StateValues;
11
+ feature(featureUri: string): StateValues;
12
+ clear(): void;
13
+ }
14
+ /** Layered state lookup: scenario overrides feature, which overrides run. */
15
+ declare class ScopedState {
16
+ private readonly runValues;
17
+ private readonly featureValues;
18
+ private readonly scenarioValues;
19
+ constructor(runValues: StateValues, featureValues: StateValues);
20
+ set<T>(scope: StateScope, key: string, value: T): T;
21
+ get<T>(key: string): T | undefined;
22
+ require<T>(key: string): T;
23
+ has(key: string): boolean;
24
+ delete(scope: StateScope, key: string): boolean;
25
+ clearScenario(): void;
26
+ private values;
27
+ }
28
+ declare const sharedState: SharedStateRegistry;
29
+
30
+ export { ScopedState, SharedStateRegistry, type StateScope, sharedState };
@@ -0,0 +1,30 @@
1
+ type StateScope = 'run' | 'feature' | 'scenario';
2
+ type StateValues = Map<string, unknown>;
3
+ /**
4
+ * Process-local state backing Behave-style run and feature context layers.
5
+ * Scenario values stay on the scenario World and are never shared.
6
+ */
7
+ declare class SharedStateRegistry {
8
+ private readonly runValues;
9
+ private readonly featureValues;
10
+ run(): StateValues;
11
+ feature(featureUri: string): StateValues;
12
+ clear(): void;
13
+ }
14
+ /** Layered state lookup: scenario overrides feature, which overrides run. */
15
+ declare class ScopedState {
16
+ private readonly runValues;
17
+ private readonly featureValues;
18
+ private readonly scenarioValues;
19
+ constructor(runValues: StateValues, featureValues: StateValues);
20
+ set<T>(scope: StateScope, key: string, value: T): T;
21
+ get<T>(key: string): T | undefined;
22
+ require<T>(key: string): T;
23
+ has(key: string): boolean;
24
+ delete(scope: StateScope, key: string): boolean;
25
+ clearScenario(): void;
26
+ private values;
27
+ }
28
+ declare const sharedState: SharedStateRegistry;
29
+
30
+ export { ScopedState, SharedStateRegistry, type StateScope, sharedState };
@@ -0,0 +1,3 @@
1
+ export { ScopedState, SharedStateRegistry, sharedState } from './chunk-HTKRJLVX.js';
2
+ //# sourceMappingURL=scoped-state.js.map
3
+ //# sourceMappingURL=scoped-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"scoped-state.js"}
package/dist/steps.cjs CHANGED
@@ -323,6 +323,142 @@ function liveOnlyReason(tags, profile) {
323
323
  if (blocking.length === 0) return null;
324
324
  return `requires a live workspace (${blocking.join(", ")}); running under DBX_TEST_PROFILE=local`;
325
325
  }
326
+ function capabilitiesFromEnv(profile, env = process.env) {
327
+ const capabilities = { profile };
328
+ const builtins = [
329
+ ["cloud", env.DBX_TEST_CLOUD],
330
+ ["compute", env.DBX_TEST_COMPUTE_MODE ?? env.DBX_TEST_COMPUTE],
331
+ ["stage", env.DBX_TEST_STAGE]
332
+ ];
333
+ for (const [key, value] of builtins) {
334
+ if (value !== void 0 && value !== "") capabilities[key] = value;
335
+ }
336
+ for (const [name, value] of Object.entries(env)) {
337
+ if (!name.startsWith("DBX_TEST_CAPABILITY_") || value === void 0) continue;
338
+ capabilities[normalizeCapabilityKey(name.slice("DBX_TEST_CAPABILITY_".length))] = value;
339
+ }
340
+ return capabilities;
341
+ }
342
+ function capabilityTagReason(tags, capabilities) {
343
+ const normalized = new Map(
344
+ Object.entries(capabilities).map(([key, value]) => [
345
+ normalizeCapabilityKey(key),
346
+ String(value).toLowerCase()
347
+ ])
348
+ );
349
+ for (const tag of tags) {
350
+ const predicate = parseCapabilityTag(tag);
351
+ if (!predicate) continue;
352
+ const actual = normalized.get(normalizeCapabilityKey(predicate.key));
353
+ const matches = predicate.expected ? actual === predicate.expected.toLowerCase() : actual !== void 0 && !["", "0", "false", "no", "off"].includes(actual);
354
+ if (predicate.kind === "requires" && !matches) {
355
+ return `${tag} requires capability ${predicate.key}${predicate.expected ? `=${predicate.expected}` : ""}; actual ${actual ?? "unset"}`;
356
+ }
357
+ if (predicate.kind === "excludes" && matches) {
358
+ return `${tag} excludes capability ${predicate.key}${predicate.expected ? `=${predicate.expected}` : ""}`;
359
+ }
360
+ }
361
+ return null;
362
+ }
363
+ function scenarioSkipReason(tags, profile, env = process.env) {
364
+ return liveOnlyReason(tags, profile) ?? capabilityTagReason(tags, capabilitiesFromEnv(profile, env));
365
+ }
366
+ function parseCapabilityTag(tag) {
367
+ const direct = tag.match(/^@(requires|excludes)\.([^=]+)(?:=(.+))?$/i);
368
+ if (direct?.[1] && direct[2]) {
369
+ return {
370
+ kind: direct[1].toLowerCase(),
371
+ key: direct[2],
372
+ ...direct[3] ? { expected: direct[3] } : {}
373
+ };
374
+ }
375
+ const behave = tag.match(/^@(use|not)\.with_([^=]+)=(.+)$/i);
376
+ if (behave?.[1] && behave[2] && behave[3]) {
377
+ return {
378
+ kind: behave[1].toLowerCase() === "use" ? "requires" : "excludes",
379
+ key: behave[2],
380
+ expected: behave[3]
381
+ };
382
+ }
383
+ return void 0;
384
+ }
385
+ function normalizeCapabilityKey(key) {
386
+ return key.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_");
387
+ }
388
+
389
+ // src/scoped-state.ts
390
+ var SharedStateRegistry = class {
391
+ runValues = /* @__PURE__ */ new Map();
392
+ featureValues = /* @__PURE__ */ new Map();
393
+ run() {
394
+ return this.runValues;
395
+ }
396
+ feature(featureUri) {
397
+ if (!featureUri) throw new Error("Feature state requires a feature URI");
398
+ let values = this.featureValues.get(featureUri);
399
+ if (!values) {
400
+ values = /* @__PURE__ */ new Map();
401
+ this.featureValues.set(featureUri, values);
402
+ }
403
+ return values;
404
+ }
405
+ clear() {
406
+ this.runValues.clear();
407
+ this.featureValues.clear();
408
+ }
409
+ };
410
+ var ScopedState = class {
411
+ constructor(runValues, featureValues) {
412
+ this.runValues = runValues;
413
+ this.featureValues = featureValues;
414
+ }
415
+ runValues;
416
+ featureValues;
417
+ scenarioValues = /* @__PURE__ */ new Map();
418
+ set(scope, key, value) {
419
+ this.values(scope).set(validateKey(key), value);
420
+ return value;
421
+ }
422
+ get(key) {
423
+ const validKey = validateKey(key);
424
+ if (this.scenarioValues.has(validKey)) return this.scenarioValues.get(validKey);
425
+ if (this.featureValues.has(validKey)) return this.featureValues.get(validKey);
426
+ return this.runValues.get(validKey);
427
+ }
428
+ require(key) {
429
+ const value = this.get(key);
430
+ if (value === void 0) throw new Error(`Required BDD state '${key}' is not set`);
431
+ return value;
432
+ }
433
+ has(key) {
434
+ const validKey = validateKey(key);
435
+ return this.scenarioValues.has(validKey) || this.featureValues.has(validKey) || this.runValues.has(validKey);
436
+ }
437
+ delete(scope, key) {
438
+ return this.values(scope).delete(validateKey(key));
439
+ }
440
+ clearScenario() {
441
+ this.scenarioValues.clear();
442
+ }
443
+ values(scope) {
444
+ if (scope === "run") return this.runValues;
445
+ if (scope === "feature") return this.featureValues;
446
+ return this.scenarioValues;
447
+ }
448
+ };
449
+ function validateKey(key) {
450
+ const trimmed = key.trim();
451
+ if (!trimmed) throw new Error("BDD state key must not be empty");
452
+ return trimmed;
453
+ }
454
+ var sharedState = new SharedStateRegistry();
455
+
456
+ // src/actions.ts
457
+ async function runAction(world, action, ...args) {
458
+ return await action(world, ...args);
459
+ }
460
+
461
+ // src/world.ts
326
462
  var DatabricksWorld = class extends cucumber.World {
327
463
  profile;
328
464
  /** Rows from the last `When I run the SQL` / `When I query table` step. */
@@ -338,10 +474,13 @@ var DatabricksWorld = class extends cucumber.World {
338
474
  lastSql;
339
475
  lastStatementId;
340
476
  featureUri;
477
+ scenarioName;
478
+ effectiveTags = [];
341
479
  stepOutputCapture;
342
480
  lakebasePool;
343
481
  ctx;
344
482
  activeCtx;
483
+ scopedState;
345
484
  cleanups = [];
346
485
  constructor(options) {
347
486
  super(options);
@@ -376,6 +515,29 @@ var DatabricksWorld = class extends cucumber.World {
376
515
  addCleanup(cleanup) {
377
516
  this.cleanups.push(cleanup);
378
517
  }
518
+ /** Initialize Behave-style feature/scenario metadata and layered state. */
519
+ initializeScenario(featureUri, scenarioName, tags) {
520
+ this.featureUri = featureUri;
521
+ this.scenarioName = scenarioName;
522
+ this.effectiveTags = [...tags];
523
+ this.scopedState = new ScopedState(sharedState.run(), sharedState.feature(featureUri));
524
+ }
525
+ /** Set typed state at run, feature, or scenario scope. */
526
+ setState(scope, key, value) {
527
+ return this.state().set(scope, key, value);
528
+ }
529
+ /** Resolve state using scenario → feature → run precedence. */
530
+ getState(key) {
531
+ return this.state().get(key);
532
+ }
533
+ /** Resolve required layered state or fail with a useful message. */
534
+ requireState(key) {
535
+ return this.state().require(key);
536
+ }
537
+ /** Compose a reusable typed action without reparsing textual Gherkin. */
538
+ runAction(action, ...args) {
539
+ return runAction(this, action, ...args);
540
+ }
379
541
  /** Setup a resource once for this Cucumber run and clean it up in AfterAll. */
380
542
  runFixture(key, setup) {
381
543
  return sharedFixtures.runFixture(key, setup);
@@ -422,8 +584,16 @@ var DatabricksWorld = class extends cucumber.World {
422
584
  errors.push(error);
423
585
  }
424
586
  this.ctx = void 0;
587
+ this.scopedState?.clearScenario();
588
+ this.scopedState = void 0;
425
589
  if (errors.length > 0) throw new AggregateError(errors, "Scenario cleanup failed");
426
590
  }
591
+ state() {
592
+ if (!this.scopedState) {
593
+ throw new Error("BDD scoped state is only available after the scenario Before hook");
594
+ }
595
+ return this.scopedState;
596
+ }
427
597
  };
428
598
 
429
599
  // src/steps.ts
@@ -443,14 +613,18 @@ cucumber.BeforeAll(async () => {
443
613
  }
444
614
  });
445
615
  cucumber.Before(function({ pickle }) {
446
- this.featureUri = pickle.uri;
447
616
  const tags = pickle.tags.map((t) => t.name);
448
- const reason = liveOnlyReason(tags, this.profile);
617
+ this.initializeScenario(pickle.uri, pickle.name, tags);
618
+ const reason = scenarioSkipReason(tags, this.profile);
449
619
  if (reason) return "skipped";
450
620
  return void 0;
451
621
  });
452
622
  cucumber.AfterAll(async () => {
453
- await sharedFixtures.dispose();
623
+ try {
624
+ await sharedFixtures.dispose();
625
+ } finally {
626
+ sharedState.clear();
627
+ }
454
628
  });
455
629
  cucumber.BeforeStep(function() {
456
630
  const captureMode = process.env.DBX_TEST_CAPTURE_OUTPUT?.toLowerCase();
@@ -642,6 +816,49 @@ cucumber.When("I run job {string}", async function(nameOrId) {
642
816
  const result = await runJobToTerminal(client, nameOrId);
643
817
  this.lastRun = { result_state: result.resultState, life_cycle_state: result.lifeCycleState };
644
818
  });
819
+ cucumber.When("I submit the job orchestration:", async function(json) {
820
+ const spec = databricksTestkit.parseJobOrchestrationLiveSpec(json);
821
+ const adapter = new databricksTestkit.DatabricksJobsAdapter(databricksTestkit.createClientFromEnv(process.env));
822
+ const runId = await adapter.submit(spec.request);
823
+ const run = await adapter.wait(runId, {
824
+ timeoutMs: Number(process.env.DBX_TEST_TIMEOUT_MS ?? 9e5),
825
+ pollIntervalMs: Number(process.env.DBX_TEST_POLL_INTERVAL_MS ?? 1e4)
826
+ });
827
+ this.lastRun = {
828
+ run_id: run.runId,
829
+ result_state: run.resultState,
830
+ life_cycle_state: run.lifeCycleState,
831
+ tasks: run.tasks
832
+ };
833
+ });
834
+ cucumber.When("I cancel the current job run", async function() {
835
+ const runId = Number(this.lastRun?.run_id);
836
+ if (!Number.isFinite(runId)) {
837
+ throw new Error("No submitted job run in this scenario");
838
+ }
839
+ const adapter = new databricksTestkit.DatabricksJobsAdapter(databricksTestkit.createClientFromEnv(process.env));
840
+ await adapter.cancel(runId);
841
+ });
842
+ cucumber.When("I repair failed tasks in the current job run", async function() {
843
+ const runId = Number(this.lastRun?.run_id);
844
+ const tasks = Array.isArray(this.lastRun?.tasks) ? this.lastRun.tasks : [];
845
+ if (!Number.isFinite(runId)) {
846
+ throw new Error("No submitted job run in this scenario");
847
+ }
848
+ const failed = tasks.filter((task) => task.resultState === "FAILED" && task.taskKey).map((task) => task.taskKey);
849
+ const adapter = new databricksTestkit.DatabricksJobsAdapter(databricksTestkit.createClientFromEnv(process.env));
850
+ await adapter.repair(runId, { rerunTasks: failed.length > 0 ? failed : void 0 });
851
+ const repaired = await adapter.wait(runId, {
852
+ timeoutMs: Number(process.env.DBX_TEST_TIMEOUT_MS ?? 9e5),
853
+ pollIntervalMs: Number(process.env.DBX_TEST_POLL_INTERVAL_MS ?? 1e4)
854
+ });
855
+ this.lastRun = {
856
+ run_id: repaired.runId,
857
+ result_state: repaired.resultState,
858
+ life_cycle_state: repaired.lifeCycleState,
859
+ tasks: repaired.tasks
860
+ };
861
+ });
645
862
  cucumber.When("I submit notebook {string}", async function(notebookPath) {
646
863
  const client = databricksTestkit.createClientFromEnv(process.env);
647
864
  await databricksTestkit.notebookSubmitCheck().run({
@@ -828,6 +1045,25 @@ cucumber.Then("the job run reaches {runState}", function(state) {
828
1045
  throw new Error(`Job run ended in ${this.lastRun.result_state}, expected ${state}`);
829
1046
  }
830
1047
  });
1048
+ cucumber.Then(
1049
+ "task {string} reaches {runState}",
1050
+ function(taskKey, state) {
1051
+ const tasks = Array.isArray(this.lastRun?.tasks) ? this.lastRun.tasks : [];
1052
+ const task = tasks.find((candidate) => candidate.taskKey === taskKey);
1053
+ if (!task) throw new Error(`Job run does not contain task '${taskKey}'`);
1054
+ if (task.resultState !== state) {
1055
+ throw new Error(
1056
+ `Task '${taskKey}' ended in ${task.resultState ?? "UNKNOWN"}, expected ${state}`
1057
+ );
1058
+ }
1059
+ }
1060
+ );
1061
+ cucumber.Then("the job run has {int} tasks", function(count) {
1062
+ const tasks = Array.isArray(this.lastRun?.tasks) ? this.lastRun.tasks : [];
1063
+ if (tasks.length !== count) {
1064
+ throw new Error(`Job run has ${tasks.length} tasks, expected ${count}`);
1065
+ }
1066
+ });
831
1067
  cucumber.Then(
832
1068
  "the pipeline update completes within {duration}",
833
1069
  async function(durationMs) {