@fabricorg/databricks-testkit 0.2.4 → 0.4.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.
package/dist/index.cjs CHANGED
@@ -654,7 +654,8 @@ async function runLiveSuite(spec, runtime = {}) {
654
654
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
655
655
  success,
656
656
  required: required2,
657
- results
657
+ results,
658
+ ...spec.targetPack ? { targetPack: spec.targetPack } : {}
658
659
  };
659
660
  if (spec.evidencePath)
660
661
  await writeReport(spec.evidencePath, `${JSON.stringify(evidence, null, 2)}
@@ -1535,6 +1536,824 @@ function classicComputeCheck() {
1535
1536
  };
1536
1537
  }
1537
1538
 
1539
+ // src/target-packs.ts
1540
+ var TARGET_PACK_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1541
+ function defineTargetPack(spec) {
1542
+ if (!TARGET_PACK_ID.test(spec.id)) {
1543
+ throw new Error(`Target pack id '${spec.id}' must be lowercase kebab-case`);
1544
+ }
1545
+ if (!spec.name.trim()) throw new Error(`Target pack '${spec.id}' requires a name`);
1546
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(spec.version)) {
1547
+ throw new Error(
1548
+ `Target pack '${spec.id}' version '${spec.version}' is not semantic versioning`
1549
+ );
1550
+ }
1551
+ if (spec.capabilities.length === 0) {
1552
+ throw new Error(`Target pack '${spec.id}' must declare at least one capability`);
1553
+ }
1554
+ const createChecks = typeof spec.checks === "function" ? spec.checks : () => spec.checks;
1555
+ const validateChecks = () => {
1556
+ const checks = [...createChecks()];
1557
+ const ids = /* @__PURE__ */ new Set();
1558
+ for (const check of checks) {
1559
+ if (!check.id.trim())
1560
+ throw new Error(`Target pack '${spec.id}' contains a check without an id`);
1561
+ if (ids.has(check.id)) {
1562
+ throw new Error(`Target pack '${spec.id}' contains duplicate check '${check.id}'`);
1563
+ }
1564
+ ids.add(check.id);
1565
+ }
1566
+ for (const required2 of spec.requiredChecks ?? []) {
1567
+ if (!ids.has(required2)) {
1568
+ throw new Error(`Target pack '${spec.id}' requires unknown check '${required2}'`);
1569
+ }
1570
+ }
1571
+ return checks;
1572
+ };
1573
+ validateChecks();
1574
+ return Object.freeze({
1575
+ ...spec,
1576
+ capabilities: Object.freeze([...spec.capabilities]),
1577
+ requiredChecks: Object.freeze([...spec.requiredChecks ?? []]),
1578
+ requirements: Object.freeze([...spec.requirements ?? []]),
1579
+ certificationScopes: Object.freeze([...spec.certificationScopes ?? []]),
1580
+ checks: validateChecks
1581
+ });
1582
+ }
1583
+ function createTargetPackRegistry(packs) {
1584
+ const registry = /* @__PURE__ */ new Map();
1585
+ for (const pack of packs) {
1586
+ if (registry.has(pack.id)) throw new Error(`Duplicate target pack '${pack.id}'`);
1587
+ registry.set(pack.id, pack);
1588
+ }
1589
+ return registry;
1590
+ }
1591
+ function doctorTargetPack(pack, env = process.env, requiredChecks = pack.requiredChecks ?? []) {
1592
+ const requirements = (pack.requirements ?? []).map((requirement) => ({
1593
+ id: requirement.id,
1594
+ description: requirement.description,
1595
+ satisfied: requirement.anyOf.some((name) => Boolean(env[name]?.trim())),
1596
+ required: requirement.required !== false,
1597
+ variables: [...requirement.anyOf]
1598
+ }));
1599
+ const required2 = new Set(requiredChecks);
1600
+ const checks = pack.checks().map((check) => ({
1601
+ id: check.id,
1602
+ description: check.description,
1603
+ configured: check.configured ? check.configured(env) : true,
1604
+ required: required2.has(check.id)
1605
+ }));
1606
+ const known = new Set(checks.map((check) => check.id));
1607
+ const missingRequirements = requirements.filter((requirement) => requirement.required && !requirement.satisfied).map((requirement) => requirement.id);
1608
+ const missingRequiredChecks = [
1609
+ ...checks.filter((check) => check.required && !check.configured).map((check) => check.id),
1610
+ ...[...required2].filter((id) => !known.has(id))
1611
+ ];
1612
+ return {
1613
+ pack: {
1614
+ id: pack.id,
1615
+ name: pack.name,
1616
+ version: pack.version,
1617
+ maturity: pack.maturity
1618
+ },
1619
+ ready: missingRequirements.length === 0 && missingRequiredChecks.length === 0,
1620
+ requirements,
1621
+ checks,
1622
+ missingRequirements,
1623
+ missingRequiredChecks
1624
+ };
1625
+ }
1626
+ function defineTargetPackRun(pack, options = {}) {
1627
+ const env = options.env ?? process.env;
1628
+ const requiredChecks = [...options.requiredChecks ?? pack.requiredChecks ?? []];
1629
+ const doctor = doctorTargetPack(pack, env, requiredChecks);
1630
+ if (!doctor.ready) {
1631
+ const missing = [...doctor.missingRequirements, ...doctor.missingRequiredChecks].join(", ");
1632
+ throw new Error(`Target pack '${pack.id}' is not configured: ${missing}`);
1633
+ }
1634
+ const targetPack = evidenceMetadata(pack, env);
1635
+ const suite = defineLiveSuite({
1636
+ checks: pack.checks(),
1637
+ required: requiredChecks,
1638
+ env,
1639
+ evidencePath: options.evidencePath,
1640
+ junitPath: options.junitPath,
1641
+ targetPack
1642
+ });
1643
+ return { pack, run: (runtime) => suite.run(runtime) };
1644
+ }
1645
+ async function runTargetPack(pack, options = {}, runtime) {
1646
+ return defineTargetPackRun(pack, options).run(runtime);
1647
+ }
1648
+ function evidenceMetadata(pack, env) {
1649
+ return {
1650
+ id: pack.id,
1651
+ name: pack.name,
1652
+ version: pack.version,
1653
+ maturity: pack.maturity,
1654
+ capabilities: [...pack.capabilities],
1655
+ ...pack.docsUrl ? { docsUrl: pack.docsUrl } : {},
1656
+ certificationScopes: [...pack.certificationScopes ?? []],
1657
+ target: compact({
1658
+ cloud: env.DBX_TEST_CLOUD,
1659
+ region: env.DBX_TEST_REGION,
1660
+ workspace: env.DATABRICKS_HOST,
1661
+ runtime: env.DBX_TEST_RUNTIME_VERSION,
1662
+ compute: env.DBX_TEST_COMPUTE
1663
+ })
1664
+ };
1665
+ }
1666
+ function compact(value) {
1667
+ return Object.fromEntries(
1668
+ Object.entries(value).filter((entry) => Boolean(entry[1]))
1669
+ );
1670
+ }
1671
+
1672
+ // src/foundation-packs.ts
1673
+ var DOCS = "https://experiments.fabric.pro/docs/testing/target-packs/";
1674
+ var AZURE_SCOPE = "Azure Databricks live-gated; see certification evidence for exact runtime";
1675
+ var workspaceRequirement = {
1676
+ id: "workspace",
1677
+ description: "Databricks workspace host",
1678
+ anyOf: ["DATABRICKS_HOST"]
1679
+ };
1680
+ var authRequirement = {
1681
+ id: "authentication",
1682
+ description: "Databricks OAuth, OIDC, bearer, or PAT authentication",
1683
+ anyOf: [
1684
+ "DATABRICKS_BEARER",
1685
+ "DATABRICKS_TOKEN",
1686
+ "DATABRICKS_CLIENT_ID",
1687
+ "DATABRICKS_OIDC_TOKEN",
1688
+ "DATABRICKS_OIDC_TOKEN_FILEPATH"
1689
+ ],
1690
+ secret: true
1691
+ };
1692
+ var warehouseRequirement = {
1693
+ id: "warehouse",
1694
+ description: "SQL warehouse ID or HTTP path",
1695
+ anyOf: ["DATABRICKS_WAREHOUSE_ID", "DATABRICKS_HTTP_PATH"]
1696
+ };
1697
+ function createSqlDeltaFoundationPack() {
1698
+ return defineTargetPack({
1699
+ id: "sql-delta",
1700
+ name: "SQL and Delta foundation",
1701
+ description: "SQL Statement Execution, Delta contracts, performance, recovery, backup and rollback.",
1702
+ version: "1.0.0",
1703
+ maturity: "live-gated",
1704
+ capabilities: [
1705
+ "sql.statement-execution",
1706
+ "delta.contract",
1707
+ "delta.time-travel",
1708
+ "performance.sql",
1709
+ "recovery.sql",
1710
+ "backup-restore.delta",
1711
+ "rollback.delta"
1712
+ ],
1713
+ checks: () => [
1714
+ restrictedPrincipalCheck(),
1715
+ sqlRoundTripCheck(),
1716
+ deltaContractCheck(),
1717
+ serverlessComputeCheck(),
1718
+ performanceBudgetCheck(),
1719
+ failureRecoveryCheck(),
1720
+ backupRestoreCheck(),
1721
+ rollbackCheck()
1722
+ ],
1723
+ requiredChecks: ["sql"],
1724
+ requirements: [workspaceRequirement, authRequirement, warehouseRequirement],
1725
+ docsUrl: `${DOCS}#sql-and-delta-foundation`,
1726
+ certificationScopes: [AZURE_SCOPE]
1727
+ });
1728
+ }
1729
+ function createLakeflowFoundationPack() {
1730
+ return defineTargetPack({
1731
+ id: "lakeflow-ingestion",
1732
+ name: "Lakeflow ingestion foundation",
1733
+ description: "Lakeflow refresh, Volumes, Auto Loader ingestion and rescued-row evidence.",
1734
+ version: "1.0.0",
1735
+ maturity: "live-gated",
1736
+ capabilities: [
1737
+ "lakeflow.refresh",
1738
+ "autoloader.ingestion",
1739
+ "volumes.files",
1740
+ "delta.rescued-data"
1741
+ ],
1742
+ checks: () => [
1743
+ restrictedPrincipalCheck(),
1744
+ pipelineRefreshCheck(),
1745
+ volumeIOCheck(),
1746
+ autoLoaderIngestionCheck()
1747
+ ],
1748
+ requiredChecks: ["pipeline"],
1749
+ requirements: [workspaceRequirement, authRequirement],
1750
+ docsUrl: `${DOCS}#lakeflow-ingestion-foundation`,
1751
+ certificationScopes: [AZURE_SCOPE]
1752
+ });
1753
+ }
1754
+ function createJobsCodeFoundationPack() {
1755
+ return defineTargetPack({
1756
+ id: "jobs-code",
1757
+ name: "Jobs and code foundation",
1758
+ description: "Jobs, notebooks, dbt, serverless compute and classic compute probes.",
1759
+ version: "1.0.0",
1760
+ maturity: "live-gated",
1761
+ capabilities: [
1762
+ "jobs.run-now",
1763
+ "jobs.submit.notebook",
1764
+ "jobs.submit.dbt",
1765
+ "compute.serverless",
1766
+ "compute.classic"
1767
+ ],
1768
+ checks: () => [
1769
+ restrictedPrincipalCheck(),
1770
+ jobRunCheck(),
1771
+ notebookSubmitCheck(),
1772
+ dbtBuildCheck(),
1773
+ serverlessComputeCheck(),
1774
+ classicComputeCheck()
1775
+ ],
1776
+ requiredChecks: ["jobs"],
1777
+ requirements: [workspaceRequirement, authRequirement],
1778
+ docsUrl: `${DOCS}#jobs-and-code-foundation`,
1779
+ certificationScopes: [AZURE_SCOPE]
1780
+ });
1781
+ }
1782
+ function createUnityStorageFoundationPack() {
1783
+ return defineTargetPack({
1784
+ id: "unity-storage",
1785
+ name: "Unity Catalog and storage foundation",
1786
+ description: "Unity Catalog allow/deny, Volumes, secret metadata and secret rotation.",
1787
+ version: "1.0.0",
1788
+ maturity: "live-gated",
1789
+ capabilities: [
1790
+ "unity-catalog.permissions",
1791
+ "volumes.files",
1792
+ "secrets.metadata",
1793
+ "secrets.rotation"
1794
+ ],
1795
+ checks: () => [
1796
+ restrictedPrincipalCheck(),
1797
+ unityCatalogAccessCheck(),
1798
+ volumeIOCheck(),
1799
+ secretScopeCheck(),
1800
+ secretRotationCheck()
1801
+ ],
1802
+ requiredChecks: ["uc-grants"],
1803
+ requirements: [workspaceRequirement, authRequirement, warehouseRequirement],
1804
+ docsUrl: `${DOCS}#unity-catalog-and-storage-foundation`,
1805
+ certificationScopes: [AZURE_SCOPE]
1806
+ });
1807
+ }
1808
+ function createAppsOperationalFoundationPack() {
1809
+ return defineTargetPack({
1810
+ id: "apps-operational",
1811
+ name: "Apps and operational data foundation",
1812
+ description: "Databricks Apps resources and health plus Lakebase OAuth and pool refresh.",
1813
+ version: "1.0.0",
1814
+ maturity: "live-gated",
1815
+ capabilities: [
1816
+ "apps.health",
1817
+ "apps.resources",
1818
+ "lakebase.credentials",
1819
+ "lakebase.round-trip",
1820
+ "lakebase.pool-refresh"
1821
+ ],
1822
+ checks: () => [
1823
+ restrictedPrincipalCheck(),
1824
+ appHealthCheck(),
1825
+ lakebaseCredentialCheck(),
1826
+ lakebaseRoundTripCheck()
1827
+ ],
1828
+ requiredChecks: ["app"],
1829
+ requirements: [workspaceRequirement, authRequirement],
1830
+ docsUrl: `${DOCS}#apps-and-operational-data-foundation`,
1831
+ certificationScopes: [AZURE_SCOPE]
1832
+ });
1833
+ }
1834
+ function createFoundationTargetPacks() {
1835
+ return [
1836
+ createSqlDeltaFoundationPack(),
1837
+ createLakeflowFoundationPack(),
1838
+ createJobsCodeFoundationPack(),
1839
+ createUnityStorageFoundationPack(),
1840
+ createAppsOperationalFoundationPack()
1841
+ ];
1842
+ }
1843
+ var foundationTargetPacks = createFoundationTargetPacks();
1844
+ var DatabricksJobsAdapter = class {
1845
+ constructor(client) {
1846
+ this.client = client;
1847
+ }
1848
+ client;
1849
+ async submit(request) {
1850
+ validateJobGraph(request.tasks);
1851
+ const response = await this.client.post(
1852
+ "/api/2.1/jobs/runs/submit",
1853
+ request
1854
+ );
1855
+ if (!response.run_id) throw new Error("Databricks Jobs submit returned no run_id");
1856
+ return response.run_id;
1857
+ }
1858
+ async get(runId) {
1859
+ const raw = await this.client.get("/api/2.1/jobs/runs/get", {
1860
+ run_id: runId,
1861
+ include_history: true,
1862
+ include_resolved_values: true
1863
+ });
1864
+ return normalizeJobRun(runId, raw);
1865
+ }
1866
+ async wait(runId, options = {}) {
1867
+ const raw = await databricks.waitForDatabricksRun(this.client, runId, {
1868
+ timeoutMs: options.timeoutMs,
1869
+ pollIntervalMs: options.pollIntervalMs
1870
+ });
1871
+ return normalizeJobRun(runId, raw);
1872
+ }
1873
+ async cancel(runId) {
1874
+ await this.client.post("/api/2.1/jobs/runs/cancel", { run_id: runId });
1875
+ }
1876
+ async waitUntilActive(runId, options = {}) {
1877
+ const timeoutMs = options.timeoutMs ?? 3e5;
1878
+ const pollIntervalMs = options.pollIntervalMs ?? 2e3;
1879
+ const deadline = Date.now() + timeoutMs;
1880
+ while (Date.now() < deadline) {
1881
+ const run = await this.get(runId);
1882
+ if (run.lifeCycleState === "RUNNING") return run;
1883
+ if (isTerminal(run.lifeCycleState)) {
1884
+ throw new Error(
1885
+ `Databricks run ${runId} reached ${run.lifeCycleState}/${run.resultState} before it could be canceled`
1886
+ );
1887
+ }
1888
+ await delay2(pollIntervalMs);
1889
+ }
1890
+ throw new Error(`Databricks run ${runId} did not become active within ${timeoutMs}ms`);
1891
+ }
1892
+ async repair(runId, options = {}) {
1893
+ const response = await this.client.post("/api/2.1/jobs/runs/repair", {
1894
+ run_id: runId,
1895
+ ...options.latestRepairId ? { latest_repair_id: options.latestRepairId } : {},
1896
+ ...options.rerunDependentTasks ? { rerun_dependent_tasks: true } : {},
1897
+ ...options.rerunTasks?.length ? { rerun_tasks: options.rerunTasks } : { rerun_all_failed_tasks: true }
1898
+ });
1899
+ if (!response.repair_id) throw new Error("Databricks Jobs repair returned no repair_id");
1900
+ return response.repair_id;
1901
+ }
1902
+ async waitForRepair(runId, repairId, options = {}) {
1903
+ const timeoutMs = options.timeoutMs ?? 9e5;
1904
+ const pollIntervalMs = options.pollIntervalMs ?? 5e3;
1905
+ const deadline = Date.now() + timeoutMs;
1906
+ while (Date.now() < deadline) {
1907
+ const run = await this.get(runId);
1908
+ const repair = run.repairs.find((candidate) => candidate.repairId === repairId);
1909
+ if (repair && isTerminal(repair.lifeCycleState)) return run;
1910
+ await delay2(pollIntervalMs);
1911
+ }
1912
+ throw new Error(
1913
+ `Databricks repair ${repairId} for run ${runId} timed out after ${timeoutMs}ms`
1914
+ );
1915
+ }
1916
+ };
1917
+ function validateJobGraph(tasks) {
1918
+ if (tasks.length === 0) throw new Error("Jobs orchestration requires at least one task");
1919
+ const keys = /* @__PURE__ */ new Set();
1920
+ for (const task of tasks) {
1921
+ if (!/^[A-Za-z0-9_-]{1,100}$/.test(task.task_key)) {
1922
+ throw new Error(`Invalid task key '${task.task_key}'`);
1923
+ }
1924
+ if (keys.has(task.task_key)) throw new Error(`Duplicate task key '${task.task_key}'`);
1925
+ keys.add(task.task_key);
1926
+ }
1927
+ const edges = /* @__PURE__ */ new Map();
1928
+ for (const task of tasks) {
1929
+ const dependencies = (task.depends_on ?? []).map((dependency) => dependency.task_key);
1930
+ for (const dependency of dependencies) {
1931
+ if (!keys.has(dependency)) {
1932
+ throw new Error(`Task '${task.task_key}' depends on unknown task '${dependency}'`);
1933
+ }
1934
+ if (dependency === task.task_key) {
1935
+ throw new Error(`Task '${task.task_key}' cannot depend on itself`);
1936
+ }
1937
+ }
1938
+ edges.set(task.task_key, dependencies);
1939
+ }
1940
+ const visiting = /* @__PURE__ */ new Set();
1941
+ const visited = /* @__PURE__ */ new Set();
1942
+ const visit = (key) => {
1943
+ if (visiting.has(key)) throw new Error(`Jobs task graph contains a cycle at '${key}'`);
1944
+ if (visited.has(key)) return;
1945
+ visiting.add(key);
1946
+ for (const dependency of edges.get(key) ?? []) visit(dependency);
1947
+ visiting.delete(key);
1948
+ visited.add(key);
1949
+ };
1950
+ for (const key of keys) visit(key);
1951
+ }
1952
+ function jobsOrchestrationCheck() {
1953
+ return {
1954
+ id: "jobs-orchestration",
1955
+ description: "Lakeflow Jobs multi-task orchestration reaches expected task outcomes",
1956
+ configured: (env) => Boolean(env.DBX_TEST_JOBS_ORCHESTRATION_JSON),
1957
+ async run({ env, client }) {
1958
+ const configured = env.DBX_TEST_JOBS_ORCHESTRATION_JSON;
1959
+ if (!configured) throw new Error("DBX_TEST_JOBS_ORCHESTRATION_JSON is required");
1960
+ const spec = parseJobOrchestrationLiveSpec(configured);
1961
+ const adapter = new DatabricksJobsAdapter(client);
1962
+ const runId = await adapter.submit(spec.request);
1963
+ let run = await adapter.wait(runId, {
1964
+ timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
1965
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 1e4)
1966
+ });
1967
+ let repairId;
1968
+ if (spec.repairFailedTasks && run.resultState !== "SUCCESS") {
1969
+ const failed = run.tasks.filter((task) => task.resultState === "FAILED").map((task) => task.taskKey);
1970
+ repairId = await adapter.repair(runId, {
1971
+ rerunTasks: failed.length > 0 ? failed : void 0,
1972
+ rerunDependentTasks: true
1973
+ });
1974
+ run = await adapter.waitForRepair(runId, repairId, {
1975
+ timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
1976
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 1e4)
1977
+ });
1978
+ }
1979
+ const expectedResult = spec.expectedResult ?? "SUCCESS";
1980
+ if (run.resultState !== expectedResult) {
1981
+ throw new Error(
1982
+ `Job orchestration run ${runId} ended in ${run.resultState}, expected ${expectedResult}`
1983
+ );
1984
+ }
1985
+ for (const [taskKey, expected] of Object.entries(spec.expectedTasks ?? {})) {
1986
+ const task = run.tasks.find((candidate) => candidate.taskKey === taskKey);
1987
+ if (!task) throw new Error(`Job orchestration result did not include task '${taskKey}'`);
1988
+ if (task.resultState !== expected) {
1989
+ throw new Error(`Task '${taskKey}' ended in ${task.resultState}, expected ${expected}`);
1990
+ }
1991
+ }
1992
+ let cancellation;
1993
+ if (spec.cancellation) {
1994
+ const cancellationRunId = await adapter.submit(spec.cancellation.request);
1995
+ await adapter.waitUntilActive(cancellationRunId, {
1996
+ timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
1997
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 1e4)
1998
+ });
1999
+ await adapter.cancel(cancellationRunId);
2000
+ const canceled = await adapter.wait(cancellationRunId, {
2001
+ timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
2002
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 1e4)
2003
+ });
2004
+ const expectedCancellation = spec.cancellation.expectedResult ?? "CANCELED";
2005
+ if (canceled.resultState !== expectedCancellation) {
2006
+ throw new Error(
2007
+ `Cancellation run ${cancellationRunId} ended in ${canceled.resultState}, expected ${expectedCancellation}`
2008
+ );
2009
+ }
2010
+ cancellation = { runId: cancellationRunId, resultState: canceled.resultState };
2011
+ }
2012
+ return {
2013
+ runId,
2014
+ ...repairId ? { repairId } : {},
2015
+ resultState: run.resultState,
2016
+ tasks: run.tasks,
2017
+ ...cancellation ? { cancellation } : {}
2018
+ };
2019
+ }
2020
+ };
2021
+ }
2022
+ function parseJobOrchestrationLiveSpec(value) {
2023
+ let parsed;
2024
+ try {
2025
+ parsed = JSON.parse(value);
2026
+ } catch (error) {
2027
+ throw new Error(`DBX_TEST_JOBS_ORCHESTRATION_JSON is invalid JSON: ${String(error)}`);
2028
+ }
2029
+ if (!parsed || typeof parsed !== "object") {
2030
+ throw new Error("Jobs orchestration spec must be an object");
2031
+ }
2032
+ const spec = parsed;
2033
+ if (!spec.request || typeof spec.request !== "object" || !Array.isArray(spec.request.tasks)) {
2034
+ throw new Error("Jobs orchestration spec requires request.tasks");
2035
+ }
2036
+ validateJobGraph(spec.request.tasks);
2037
+ if (spec.cancellation) {
2038
+ if (typeof spec.cancellation !== "object" || !spec.cancellation.request || !Array.isArray(spec.cancellation.request.tasks)) {
2039
+ throw new Error("Jobs orchestration cancellation requires request.tasks");
2040
+ }
2041
+ validateJobGraph(spec.cancellation.request.tasks);
2042
+ }
2043
+ return spec;
2044
+ }
2045
+ function normalizeJobRun(fallbackRunId, raw) {
2046
+ const state = objectOf(raw.state);
2047
+ const repairEntries = Array.isArray(raw.repair_history) ? raw.repair_history.filter(
2048
+ (repair) => Boolean(repair && typeof repair === "object")
2049
+ ) : [];
2050
+ const taskRunRanks = /* @__PURE__ */ new Map();
2051
+ for (const [rank, repair] of repairEntries.entries()) {
2052
+ for (const runId of numberArray(repair.task_run_ids)) taskRunRanks.set(runId, rank);
2053
+ }
2054
+ const taskAttempts = Array.isArray(raw.tasks) ? raw.tasks.filter(
2055
+ (task) => Boolean(task && typeof task === "object")
2056
+ ).map((task) => {
2057
+ const taskState = objectOf(task.state);
2058
+ return {
2059
+ taskKey: String(task.task_key ?? "unknown"),
2060
+ ...Number.isFinite(Number(task.run_id)) ? { runId: Number(task.run_id) } : {},
2061
+ lifeCycleState: String(taskState.life_cycle_state ?? "UNKNOWN"),
2062
+ resultState: String(taskState.result_state ?? "UNKNOWN"),
2063
+ ...typeof taskState.state_message === "string" ? { stateMessage: taskState.state_message } : {},
2064
+ ...Number.isFinite(Number(task.attempt_number)) ? { attemptNumber: Number(task.attempt_number) } : {}
2065
+ };
2066
+ }) : [];
2067
+ const latestTasks = /* @__PURE__ */ new Map();
2068
+ for (const task of taskAttempts) {
2069
+ const current = latestTasks.get(task.taskKey);
2070
+ const currentAttempt = current?.attemptNumber ?? -1;
2071
+ const candidateAttempt = task.attemptNumber ?? -1;
2072
+ const currentRank = current?.runId ? taskRunRanks.get(current.runId) ?? -1 : -1;
2073
+ const candidateRank = task.runId ? taskRunRanks.get(task.runId) ?? -1 : -1;
2074
+ if (!current || candidateAttempt > currentAttempt || candidateAttempt === currentAttempt && candidateRank >= currentRank) {
2075
+ latestTasks.set(task.taskKey, task);
2076
+ }
2077
+ }
2078
+ const tasks = [...latestTasks.values()];
2079
+ const repairs = repairEntries.map((repair) => {
2080
+ const repairState = objectOf(repair.state);
2081
+ return {
2082
+ repairId: Number(repair.id ?? repair.repair_id ?? 0),
2083
+ type: String(repair.type ?? "UNKNOWN"),
2084
+ lifeCycleState: String(repairState.life_cycle_state ?? "UNKNOWN"),
2085
+ resultState: String(repairState.result_state ?? "UNKNOWN"),
2086
+ taskRunIds: numberArray(repair.task_run_ids)
2087
+ };
2088
+ });
2089
+ return {
2090
+ runId: Number(raw.run_id ?? fallbackRunId),
2091
+ lifeCycleState: String(state.life_cycle_state ?? "UNKNOWN"),
2092
+ resultState: String(state.result_state ?? "UNKNOWN"),
2093
+ tasks,
2094
+ repairs,
2095
+ raw
2096
+ };
2097
+ }
2098
+ function isTerminal(state) {
2099
+ return ["TERMINATED", "SKIPPED", "INTERNAL_ERROR", "BLOCKED"].includes(state);
2100
+ }
2101
+ function delay2(ms) {
2102
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2103
+ }
2104
+ function objectOf(value) {
2105
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2106
+ }
2107
+ function numberArray(value) {
2108
+ return Array.isArray(value) ? value.map(Number).filter((candidate) => Number.isFinite(candidate)) : [];
2109
+ }
2110
+
2111
+ // src/jobs-certification-check.ts
2112
+ var NOTEBOOKS = {
2113
+ seed: `# Databricks notebook source
2114
+ dbutils.jobs.taskValues.set(key="branch", value="run")
2115
+ dbutils.jobs.taskValues.set(key="items", value=["alpha", "beta"])
2116
+ dbutils.notebook.exit("seeded")
2117
+ `,
2118
+ process: `# Databricks notebook source
2119
+ dbutils.widgets.text("item", "")
2120
+ item = dbutils.widgets.get("item")
2121
+ assert item, "item parameter is required"
2122
+ dbutils.notebook.exit(item)
2123
+ `,
2124
+ retry: `# Databricks notebook source
2125
+ dbutils.widgets.text("execution_count", "0")
2126
+ execution_count = int(dbutils.widgets.get("execution_count"))
2127
+ if execution_count <= 1:
2128
+ raise RuntimeError("intentional retryable failure")
2129
+ dbutils.notebook.exit("retried")
2130
+ `,
2131
+ repair: `# Databricks notebook source
2132
+ dbutils.widgets.text("repair_count", "0")
2133
+ repair_count = int(dbutils.widgets.get("repair_count"))
2134
+ if repair_count == 0:
2135
+ raise RuntimeError("intentional first-run failure")
2136
+ dbutils.notebook.exit("repaired")
2137
+ `,
2138
+ sleep: `# Databricks notebook source
2139
+ import time
2140
+ time.sleep(300)
2141
+ `
2142
+ };
2143
+ function jobsCertificationCheck() {
2144
+ return {
2145
+ id: "jobs-certification",
2146
+ description: "Disposable Lakeflow Jobs branch, loop, failure, repair, cancellation, and cleanup certification",
2147
+ configured: (env) => env.DBX_TEST_JOBS_CERTIFY === "1",
2148
+ async run({ env, client }) {
2149
+ const root = `${env.DBX_TEST_JOBS_FIXTURE_ROOT ?? "/Workspace/Shared/fabric-experiments-target-pack"}/${crypto.randomUUID()}`;
2150
+ const paths = Object.fromEntries(
2151
+ Object.keys(NOTEBOOKS).map((name) => [name, `${root}/${name}`])
2152
+ );
2153
+ try {
2154
+ await importNotebooks(client, paths);
2155
+ const adapter = new DatabricksJobsAdapter(client);
2156
+ const runId = await adapter.submit(certificationRequest(paths));
2157
+ const initial = await adapter.wait(runId, waitOptions(env));
2158
+ if (initial.resultState !== "FAILED") {
2159
+ throw new Error(
2160
+ `Certification run ${runId} expected FAILED before repair, got ${initial.resultState}`
2161
+ );
2162
+ }
2163
+ const retried = assertTask(initial.tasks, "retry", "SUCCESS");
2164
+ if ((retried.attemptNumber ?? 0) < 1) {
2165
+ throw new Error("Certification retry task did not record a second attempt");
2166
+ }
2167
+ assertTask(initial.tasks, "seed", "SUCCESS");
2168
+ assertTask(initial.tasks, "branch", "SUCCESS");
2169
+ assertTask(initial.tasks, "foreach", "SUCCESS");
2170
+ assertTask(initial.tasks, "repair", "FAILED");
2171
+ const repairId = await adapter.repair(runId, {
2172
+ rerunTasks: ["repair"],
2173
+ rerunDependentTasks: true
2174
+ });
2175
+ const repaired = await adapter.waitForRepair(runId, repairId, waitOptions(env));
2176
+ if (repaired.resultState !== "SUCCESS") {
2177
+ throw new Error(`Certification repair ${repairId} ended in ${repaired.resultState}`);
2178
+ }
2179
+ assertTask(repaired.tasks, "repair", "SUCCESS");
2180
+ const cancellationRunId = await adapter.submit(cancellationRequest(paths));
2181
+ await adapter.waitUntilActive(cancellationRunId, waitOptions(env));
2182
+ await adapter.cancel(cancellationRunId);
2183
+ const canceled = await adapter.wait(cancellationRunId, waitOptions(env));
2184
+ if (canceled.resultState !== "CANCELED") {
2185
+ throw new Error(`Certification cancellation ended in ${canceled.resultState}`);
2186
+ }
2187
+ return {
2188
+ runId,
2189
+ repairId,
2190
+ cancellationRunId,
2191
+ taskValues: true,
2192
+ retry: true,
2193
+ condition: true,
2194
+ forEach: true,
2195
+ repair: true,
2196
+ cancellation: true,
2197
+ fixtureRoot: root
2198
+ };
2199
+ } finally {
2200
+ await client.post("/api/2.0/workspace/delete", { path: root, recursive: true });
2201
+ }
2202
+ }
2203
+ };
2204
+ }
2205
+ async function importNotebooks(client, paths) {
2206
+ await client.post("/api/2.0/workspace/mkdirs", {
2207
+ path: paths.seed.slice(0, paths.seed.lastIndexOf("/"))
2208
+ });
2209
+ for (const [name, source] of Object.entries(NOTEBOOKS)) {
2210
+ await client.post("/api/2.0/workspace/import", {
2211
+ path: paths[name],
2212
+ format: "SOURCE",
2213
+ language: "PYTHON",
2214
+ overwrite: true,
2215
+ content: Buffer.from(source).toString("base64")
2216
+ });
2217
+ }
2218
+ }
2219
+ function certificationRequest(paths) {
2220
+ return {
2221
+ run_name: "fabric-experiments-jobs-certification",
2222
+ tasks: [
2223
+ {
2224
+ task_key: "retry",
2225
+ notebook_task: {
2226
+ notebook_path: paths.retry,
2227
+ base_parameters: { execution_count: "{{task.execution_count}}" }
2228
+ },
2229
+ max_retries: 1
2230
+ },
2231
+ {
2232
+ task_key: "seed",
2233
+ depends_on: [{ task_key: "retry" }],
2234
+ notebook_task: { notebook_path: paths.seed }
2235
+ },
2236
+ {
2237
+ task_key: "branch",
2238
+ depends_on: [{ task_key: "seed" }],
2239
+ condition_task: {
2240
+ left: "{{tasks.seed.values.branch}}",
2241
+ op: "EQUAL_TO",
2242
+ right: "run"
2243
+ }
2244
+ },
2245
+ {
2246
+ task_key: "foreach",
2247
+ depends_on: [{ task_key: "branch", outcome: "true" }],
2248
+ for_each_task: {
2249
+ inputs: "{{tasks.seed.values.items}}",
2250
+ concurrency: 2,
2251
+ task: {
2252
+ task_key: "process_item",
2253
+ notebook_task: {
2254
+ notebook_path: paths.process,
2255
+ base_parameters: { item: "{{input}}" }
2256
+ }
2257
+ }
2258
+ }
2259
+ },
2260
+ {
2261
+ task_key: "repair",
2262
+ depends_on: [{ task_key: "foreach" }],
2263
+ notebook_task: {
2264
+ notebook_path: paths.repair,
2265
+ base_parameters: { repair_count: "{{job.repair_count}}" }
2266
+ }
2267
+ }
2268
+ ]
2269
+ };
2270
+ }
2271
+ function cancellationRequest(paths) {
2272
+ return {
2273
+ run_name: "fabric-experiments-jobs-cancellation-certification",
2274
+ tasks: [{ task_key: "sleep", notebook_task: { notebook_path: paths.sleep } }]
2275
+ };
2276
+ }
2277
+ function waitOptions(env) {
2278
+ return {
2279
+ timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
2280
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 5e3)
2281
+ };
2282
+ }
2283
+ function assertTask(tasks, taskKey, resultState) {
2284
+ const task = tasks.find((candidate) => candidate.taskKey === taskKey);
2285
+ if (!task) throw new Error(`Certification result did not contain task '${taskKey}'`);
2286
+ if (task.resultState !== resultState) {
2287
+ throw new Error(
2288
+ `Certification task '${taskKey}' ended in ${task.resultState}, expected ${resultState}`
2289
+ );
2290
+ }
2291
+ return task;
2292
+ }
2293
+
2294
+ // src/jobs-target-pack.ts
2295
+ function createLakeflowJobsTargetPack() {
2296
+ return defineTargetPack({
2297
+ id: "lakeflow-jobs",
2298
+ name: "Lakeflow Jobs orchestration",
2299
+ description: "Typed multi-task DAG, branch/loop, retry, cancellation, repair and task-outcome validation.",
2300
+ version: "0.2.0",
2301
+ maturity: "live-gated",
2302
+ capabilities: [
2303
+ "jobs.multi-task-dag",
2304
+ "jobs.dependencies",
2305
+ "jobs.condition-task",
2306
+ "jobs.for-each-task",
2307
+ "jobs.retry",
2308
+ "jobs.cancel",
2309
+ "jobs.repair",
2310
+ "jobs.task-outcomes"
2311
+ ],
2312
+ checks: () => [
2313
+ restrictedPrincipalCheck(),
2314
+ jobRunCheck(),
2315
+ jobsOrchestrationCheck(),
2316
+ jobsCertificationCheck()
2317
+ ],
2318
+ requiredChecks: ["jobs-orchestration"],
2319
+ requirements: [
2320
+ {
2321
+ id: "workspace",
2322
+ description: "Databricks workspace host",
2323
+ anyOf: ["DATABRICKS_HOST"]
2324
+ },
2325
+ {
2326
+ id: "authentication",
2327
+ description: "Databricks OAuth, OIDC, bearer, or PAT authentication",
2328
+ anyOf: [
2329
+ "DATABRICKS_BEARER",
2330
+ "DATABRICKS_TOKEN",
2331
+ "DATABRICKS_CLIENT_ID",
2332
+ "DATABRICKS_OIDC_TOKEN",
2333
+ "DATABRICKS_OIDC_TOKEN_FILEPATH"
2334
+ ],
2335
+ secret: true
2336
+ },
2337
+ {
2338
+ id: "orchestration-spec",
2339
+ description: "JSON multi-task orchestration scenario",
2340
+ anyOf: ["DBX_TEST_JOBS_ORCHESTRATION_JSON"],
2341
+ required: false
2342
+ }
2343
+ ],
2344
+ docsUrl: "https://experiments.fabric.pro/docs/testing/target-packs/#lakeflow-jobs-orchestration",
2345
+ certificationScopes: [
2346
+ "Azure Databricks serverless Jobs, West US, workspace OAuth, public network path"
2347
+ ]
2348
+ });
2349
+ }
2350
+
2351
+ // src/builtin-packs.ts
2352
+ function createBuiltinTargetPacks() {
2353
+ return [...createFoundationTargetPacks(), createLakeflowJobsTargetPack()];
2354
+ }
2355
+ var builtinTargetPacks = createBuiltinTargetPacks();
2356
+
1538
2357
  Object.defineProperty(exports, "waitForDatabricksRun", {
1539
2358
  enumerable: true,
1540
2359
  get: function () { return databricks.waitForDatabricksRun; }
@@ -1543,36 +2362,55 @@ Object.defineProperty(exports, "waitForDatabricksStatement", {
1543
2362
  enumerable: true,
1544
2363
  get: function () { return databricks.waitForDatabricksStatement; }
1545
2364
  });
2365
+ exports.DatabricksJobsAdapter = DatabricksJobsAdapter;
1546
2366
  exports.appHealthCheck = appHealthCheck;
1547
2367
  exports.assertDeltaContract = assertDeltaContract;
1548
2368
  exports.autoLoaderIngestionCheck = autoLoaderIngestionCheck;
1549
2369
  exports.backupRestoreCheck = backupRestoreCheck;
2370
+ exports.builtinTargetPacks = builtinTargetPacks;
1550
2371
  exports.classicComputeCheck = classicComputeCheck;
1551
2372
  exports.compareToGolden = compareToGolden;
2373
+ exports.createAppsOperationalFoundationPack = createAppsOperationalFoundationPack;
2374
+ exports.createBuiltinTargetPacks = createBuiltinTargetPacks;
1552
2375
  exports.createClientFromEnv = createClientFromEnv;
1553
2376
  exports.createDuckDbContext = createDuckDbContext;
1554
2377
  exports.createEphemeralLakebaseBranch = createEphemeralLakebaseBranch;
1555
2378
  exports.createExecutionContext = createExecutionContext;
2379
+ exports.createFoundationTargetPacks = createFoundationTargetPacks;
2380
+ exports.createJobsCodeFoundationPack = createJobsCodeFoundationPack;
1556
2381
  exports.createLakebaseTestProvider = createLakebaseTestProvider;
2382
+ exports.createLakeflowFoundationPack = createLakeflowFoundationPack;
2383
+ exports.createLakeflowJobsTargetPack = createLakeflowJobsTargetPack;
1557
2384
  exports.createMockDatabricksClient = createMockDatabricksClient;
2385
+ exports.createSqlDeltaFoundationPack = createSqlDeltaFoundationPack;
2386
+ exports.createTargetPackRegistry = createTargetPackRegistry;
2387
+ exports.createUnityStorageFoundationPack = createUnityStorageFoundationPack;
1558
2388
  exports.createWorkspaceContext = createWorkspaceContext;
1559
2389
  exports.customLiveCheck = customLiveCheck;
1560
2390
  exports.dbtBuildCheck = dbtBuildCheck;
1561
2391
  exports.defineLiveSuite = defineLiveSuite;
2392
+ exports.defineTargetPack = defineTargetPack;
2393
+ exports.defineTargetPackRun = defineTargetPackRun;
1562
2394
  exports.deleteEphemeralLakebaseBranch = deleteEphemeralLakebaseBranch;
1563
2395
  exports.deleteVolumeFile = deleteVolumeFile;
1564
2396
  exports.deltaContractCheck = deltaContractCheck;
2397
+ exports.doctorTargetPack = doctorTargetPack;
1565
2398
  exports.ensureVolumeDirectory = ensureVolumeDirectory;
1566
2399
  exports.expectQueryToMatchGolden = expectQueryToMatchGolden;
1567
2400
  exports.expectTable = expectTable;
1568
2401
  exports.failureRecoveryCheck = failureRecoveryCheck;
2402
+ exports.foundationTargetPacks = foundationTargetPacks;
1569
2403
  exports.jobRunCheck = jobRunCheck;
2404
+ exports.jobsCertificationCheck = jobsCertificationCheck;
2405
+ exports.jobsOrchestrationCheck = jobsOrchestrationCheck;
1570
2406
  exports.lakebaseCredentialCheck = lakebaseCredentialCheck;
1571
2407
  exports.lakebaseRoundTripCheck = lakebaseRoundTripCheck;
2408
+ exports.normalizeJobRun = normalizeJobRun;
1572
2409
  exports.normalizeRow = normalizeRow;
1573
2410
  exports.normalizeVolumeRoot = normalizeVolumeRoot;
1574
2411
  exports.notebookSubmitCheck = notebookSubmitCheck;
1575
2412
  exports.parseFixtureColumns = parseFixtureColumns;
2413
+ exports.parseJobOrchestrationLiveSpec = parseJobOrchestrationLiveSpec;
1576
2414
  exports.parseStatementRows = parseStatementRows;
1577
2415
  exports.performanceBudgetCheck = performanceBudgetCheck;
1578
2416
  exports.pipelineRefreshCheck = pipelineRefreshCheck;
@@ -1584,6 +2422,7 @@ exports.resolveTokenProvider = resolveTokenProvider;
1584
2422
  exports.restrictedPrincipalCheck = restrictedPrincipalCheck;
1585
2423
  exports.rollbackCheck = rollbackCheck;
1586
2424
  exports.runLiveSuite = runLiveSuite;
2425
+ exports.runTargetPack = runTargetPack;
1587
2426
  exports.secretRotationCheck = secretRotationCheck;
1588
2427
  exports.secretScopeCheck = secretScopeCheck;
1589
2428
  exports.serverlessComputeCheck = serverlessComputeCheck;
@@ -1592,6 +2431,7 @@ exports.toLakebaseBranchId = toLakebaseBranchId;
1592
2431
  exports.toNamedParameters = toNamedParameters;
1593
2432
  exports.unityCatalogAccessCheck = unityCatalogAccessCheck;
1594
2433
  exports.uploadVolumeFile = uploadVolumeFile;
2434
+ exports.validateJobGraph = validateJobGraph;
1595
2435
  exports.volumeIOCheck = volumeIOCheck;
1596
2436
  exports.waitForPipelineUpdate = waitForPipelineUpdate;
1597
2437
  //# sourceMappingURL=index.cjs.map