@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.js CHANGED
@@ -196,7 +196,8 @@ async function runLiveSuite(spec, runtime = {}) {
196
196
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
197
197
  success,
198
198
  required: required2,
199
- results
199
+ results,
200
+ ...spec.targetPack ? { targetPack: spec.targetPack } : {}
200
201
  };
201
202
  if (spec.evidencePath)
202
203
  await writeReport(spec.evidencePath, `${JSON.stringify(evidence, null, 2)}
@@ -1072,6 +1073,824 @@ function classicComputeCheck() {
1072
1073
  };
1073
1074
  }
1074
1075
 
1075
- 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 };
1076
+ // src/target-packs.ts
1077
+ var TARGET_PACK_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1078
+ function defineTargetPack(spec) {
1079
+ if (!TARGET_PACK_ID.test(spec.id)) {
1080
+ throw new Error(`Target pack id '${spec.id}' must be lowercase kebab-case`);
1081
+ }
1082
+ if (!spec.name.trim()) throw new Error(`Target pack '${spec.id}' requires a name`);
1083
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(spec.version)) {
1084
+ throw new Error(
1085
+ `Target pack '${spec.id}' version '${spec.version}' is not semantic versioning`
1086
+ );
1087
+ }
1088
+ if (spec.capabilities.length === 0) {
1089
+ throw new Error(`Target pack '${spec.id}' must declare at least one capability`);
1090
+ }
1091
+ const createChecks = typeof spec.checks === "function" ? spec.checks : () => spec.checks;
1092
+ const validateChecks = () => {
1093
+ const checks = [...createChecks()];
1094
+ const ids = /* @__PURE__ */ new Set();
1095
+ for (const check of checks) {
1096
+ if (!check.id.trim())
1097
+ throw new Error(`Target pack '${spec.id}' contains a check without an id`);
1098
+ if (ids.has(check.id)) {
1099
+ throw new Error(`Target pack '${spec.id}' contains duplicate check '${check.id}'`);
1100
+ }
1101
+ ids.add(check.id);
1102
+ }
1103
+ for (const required2 of spec.requiredChecks ?? []) {
1104
+ if (!ids.has(required2)) {
1105
+ throw new Error(`Target pack '${spec.id}' requires unknown check '${required2}'`);
1106
+ }
1107
+ }
1108
+ return checks;
1109
+ };
1110
+ validateChecks();
1111
+ return Object.freeze({
1112
+ ...spec,
1113
+ capabilities: Object.freeze([...spec.capabilities]),
1114
+ requiredChecks: Object.freeze([...spec.requiredChecks ?? []]),
1115
+ requirements: Object.freeze([...spec.requirements ?? []]),
1116
+ certificationScopes: Object.freeze([...spec.certificationScopes ?? []]),
1117
+ checks: validateChecks
1118
+ });
1119
+ }
1120
+ function createTargetPackRegistry(packs) {
1121
+ const registry = /* @__PURE__ */ new Map();
1122
+ for (const pack of packs) {
1123
+ if (registry.has(pack.id)) throw new Error(`Duplicate target pack '${pack.id}'`);
1124
+ registry.set(pack.id, pack);
1125
+ }
1126
+ return registry;
1127
+ }
1128
+ function doctorTargetPack(pack, env = process.env, requiredChecks = pack.requiredChecks ?? []) {
1129
+ const requirements = (pack.requirements ?? []).map((requirement) => ({
1130
+ id: requirement.id,
1131
+ description: requirement.description,
1132
+ satisfied: requirement.anyOf.some((name) => Boolean(env[name]?.trim())),
1133
+ required: requirement.required !== false,
1134
+ variables: [...requirement.anyOf]
1135
+ }));
1136
+ const required2 = new Set(requiredChecks);
1137
+ const checks = pack.checks().map((check) => ({
1138
+ id: check.id,
1139
+ description: check.description,
1140
+ configured: check.configured ? check.configured(env) : true,
1141
+ required: required2.has(check.id)
1142
+ }));
1143
+ const known = new Set(checks.map((check) => check.id));
1144
+ const missingRequirements = requirements.filter((requirement) => requirement.required && !requirement.satisfied).map((requirement) => requirement.id);
1145
+ const missingRequiredChecks = [
1146
+ ...checks.filter((check) => check.required && !check.configured).map((check) => check.id),
1147
+ ...[...required2].filter((id) => !known.has(id))
1148
+ ];
1149
+ return {
1150
+ pack: {
1151
+ id: pack.id,
1152
+ name: pack.name,
1153
+ version: pack.version,
1154
+ maturity: pack.maturity
1155
+ },
1156
+ ready: missingRequirements.length === 0 && missingRequiredChecks.length === 0,
1157
+ requirements,
1158
+ checks,
1159
+ missingRequirements,
1160
+ missingRequiredChecks
1161
+ };
1162
+ }
1163
+ function defineTargetPackRun(pack, options = {}) {
1164
+ const env = options.env ?? process.env;
1165
+ const requiredChecks = [...options.requiredChecks ?? pack.requiredChecks ?? []];
1166
+ const doctor = doctorTargetPack(pack, env, requiredChecks);
1167
+ if (!doctor.ready) {
1168
+ const missing = [...doctor.missingRequirements, ...doctor.missingRequiredChecks].join(", ");
1169
+ throw new Error(`Target pack '${pack.id}' is not configured: ${missing}`);
1170
+ }
1171
+ const targetPack = evidenceMetadata(pack, env);
1172
+ const suite = defineLiveSuite({
1173
+ checks: pack.checks(),
1174
+ required: requiredChecks,
1175
+ env,
1176
+ evidencePath: options.evidencePath,
1177
+ junitPath: options.junitPath,
1178
+ targetPack
1179
+ });
1180
+ return { pack, run: (runtime) => suite.run(runtime) };
1181
+ }
1182
+ async function runTargetPack(pack, options = {}, runtime) {
1183
+ return defineTargetPackRun(pack, options).run(runtime);
1184
+ }
1185
+ function evidenceMetadata(pack, env) {
1186
+ return {
1187
+ id: pack.id,
1188
+ name: pack.name,
1189
+ version: pack.version,
1190
+ maturity: pack.maturity,
1191
+ capabilities: [...pack.capabilities],
1192
+ ...pack.docsUrl ? { docsUrl: pack.docsUrl } : {},
1193
+ certificationScopes: [...pack.certificationScopes ?? []],
1194
+ target: compact({
1195
+ cloud: env.DBX_TEST_CLOUD,
1196
+ region: env.DBX_TEST_REGION,
1197
+ workspace: env.DATABRICKS_HOST,
1198
+ runtime: env.DBX_TEST_RUNTIME_VERSION,
1199
+ compute: env.DBX_TEST_COMPUTE
1200
+ })
1201
+ };
1202
+ }
1203
+ function compact(value) {
1204
+ return Object.fromEntries(
1205
+ Object.entries(value).filter((entry) => Boolean(entry[1]))
1206
+ );
1207
+ }
1208
+
1209
+ // src/foundation-packs.ts
1210
+ var DOCS = "https://experiments.fabric.pro/docs/testing/target-packs/";
1211
+ var AZURE_SCOPE = "Azure Databricks live-gated; see certification evidence for exact runtime";
1212
+ var workspaceRequirement = {
1213
+ id: "workspace",
1214
+ description: "Databricks workspace host",
1215
+ anyOf: ["DATABRICKS_HOST"]
1216
+ };
1217
+ var authRequirement = {
1218
+ id: "authentication",
1219
+ description: "Databricks OAuth, OIDC, bearer, or PAT authentication",
1220
+ anyOf: [
1221
+ "DATABRICKS_BEARER",
1222
+ "DATABRICKS_TOKEN",
1223
+ "DATABRICKS_CLIENT_ID",
1224
+ "DATABRICKS_OIDC_TOKEN",
1225
+ "DATABRICKS_OIDC_TOKEN_FILEPATH"
1226
+ ],
1227
+ secret: true
1228
+ };
1229
+ var warehouseRequirement = {
1230
+ id: "warehouse",
1231
+ description: "SQL warehouse ID or HTTP path",
1232
+ anyOf: ["DATABRICKS_WAREHOUSE_ID", "DATABRICKS_HTTP_PATH"]
1233
+ };
1234
+ function createSqlDeltaFoundationPack() {
1235
+ return defineTargetPack({
1236
+ id: "sql-delta",
1237
+ name: "SQL and Delta foundation",
1238
+ description: "SQL Statement Execution, Delta contracts, performance, recovery, backup and rollback.",
1239
+ version: "1.0.0",
1240
+ maturity: "live-gated",
1241
+ capabilities: [
1242
+ "sql.statement-execution",
1243
+ "delta.contract",
1244
+ "delta.time-travel",
1245
+ "performance.sql",
1246
+ "recovery.sql",
1247
+ "backup-restore.delta",
1248
+ "rollback.delta"
1249
+ ],
1250
+ checks: () => [
1251
+ restrictedPrincipalCheck(),
1252
+ sqlRoundTripCheck(),
1253
+ deltaContractCheck(),
1254
+ serverlessComputeCheck(),
1255
+ performanceBudgetCheck(),
1256
+ failureRecoveryCheck(),
1257
+ backupRestoreCheck(),
1258
+ rollbackCheck()
1259
+ ],
1260
+ requiredChecks: ["sql"],
1261
+ requirements: [workspaceRequirement, authRequirement, warehouseRequirement],
1262
+ docsUrl: `${DOCS}#sql-and-delta-foundation`,
1263
+ certificationScopes: [AZURE_SCOPE]
1264
+ });
1265
+ }
1266
+ function createLakeflowFoundationPack() {
1267
+ return defineTargetPack({
1268
+ id: "lakeflow-ingestion",
1269
+ name: "Lakeflow ingestion foundation",
1270
+ description: "Lakeflow refresh, Volumes, Auto Loader ingestion and rescued-row evidence.",
1271
+ version: "1.0.0",
1272
+ maturity: "live-gated",
1273
+ capabilities: [
1274
+ "lakeflow.refresh",
1275
+ "autoloader.ingestion",
1276
+ "volumes.files",
1277
+ "delta.rescued-data"
1278
+ ],
1279
+ checks: () => [
1280
+ restrictedPrincipalCheck(),
1281
+ pipelineRefreshCheck(),
1282
+ volumeIOCheck(),
1283
+ autoLoaderIngestionCheck()
1284
+ ],
1285
+ requiredChecks: ["pipeline"],
1286
+ requirements: [workspaceRequirement, authRequirement],
1287
+ docsUrl: `${DOCS}#lakeflow-ingestion-foundation`,
1288
+ certificationScopes: [AZURE_SCOPE]
1289
+ });
1290
+ }
1291
+ function createJobsCodeFoundationPack() {
1292
+ return defineTargetPack({
1293
+ id: "jobs-code",
1294
+ name: "Jobs and code foundation",
1295
+ description: "Jobs, notebooks, dbt, serverless compute and classic compute probes.",
1296
+ version: "1.0.0",
1297
+ maturity: "live-gated",
1298
+ capabilities: [
1299
+ "jobs.run-now",
1300
+ "jobs.submit.notebook",
1301
+ "jobs.submit.dbt",
1302
+ "compute.serverless",
1303
+ "compute.classic"
1304
+ ],
1305
+ checks: () => [
1306
+ restrictedPrincipalCheck(),
1307
+ jobRunCheck(),
1308
+ notebookSubmitCheck(),
1309
+ dbtBuildCheck(),
1310
+ serverlessComputeCheck(),
1311
+ classicComputeCheck()
1312
+ ],
1313
+ requiredChecks: ["jobs"],
1314
+ requirements: [workspaceRequirement, authRequirement],
1315
+ docsUrl: `${DOCS}#jobs-and-code-foundation`,
1316
+ certificationScopes: [AZURE_SCOPE]
1317
+ });
1318
+ }
1319
+ function createUnityStorageFoundationPack() {
1320
+ return defineTargetPack({
1321
+ id: "unity-storage",
1322
+ name: "Unity Catalog and storage foundation",
1323
+ description: "Unity Catalog allow/deny, Volumes, secret metadata and secret rotation.",
1324
+ version: "1.0.0",
1325
+ maturity: "live-gated",
1326
+ capabilities: [
1327
+ "unity-catalog.permissions",
1328
+ "volumes.files",
1329
+ "secrets.metadata",
1330
+ "secrets.rotation"
1331
+ ],
1332
+ checks: () => [
1333
+ restrictedPrincipalCheck(),
1334
+ unityCatalogAccessCheck(),
1335
+ volumeIOCheck(),
1336
+ secretScopeCheck(),
1337
+ secretRotationCheck()
1338
+ ],
1339
+ requiredChecks: ["uc-grants"],
1340
+ requirements: [workspaceRequirement, authRequirement, warehouseRequirement],
1341
+ docsUrl: `${DOCS}#unity-catalog-and-storage-foundation`,
1342
+ certificationScopes: [AZURE_SCOPE]
1343
+ });
1344
+ }
1345
+ function createAppsOperationalFoundationPack() {
1346
+ return defineTargetPack({
1347
+ id: "apps-operational",
1348
+ name: "Apps and operational data foundation",
1349
+ description: "Databricks Apps resources and health plus Lakebase OAuth and pool refresh.",
1350
+ version: "1.0.0",
1351
+ maturity: "live-gated",
1352
+ capabilities: [
1353
+ "apps.health",
1354
+ "apps.resources",
1355
+ "lakebase.credentials",
1356
+ "lakebase.round-trip",
1357
+ "lakebase.pool-refresh"
1358
+ ],
1359
+ checks: () => [
1360
+ restrictedPrincipalCheck(),
1361
+ appHealthCheck(),
1362
+ lakebaseCredentialCheck(),
1363
+ lakebaseRoundTripCheck()
1364
+ ],
1365
+ requiredChecks: ["app"],
1366
+ requirements: [workspaceRequirement, authRequirement],
1367
+ docsUrl: `${DOCS}#apps-and-operational-data-foundation`,
1368
+ certificationScopes: [AZURE_SCOPE]
1369
+ });
1370
+ }
1371
+ function createFoundationTargetPacks() {
1372
+ return [
1373
+ createSqlDeltaFoundationPack(),
1374
+ createLakeflowFoundationPack(),
1375
+ createJobsCodeFoundationPack(),
1376
+ createUnityStorageFoundationPack(),
1377
+ createAppsOperationalFoundationPack()
1378
+ ];
1379
+ }
1380
+ var foundationTargetPacks = createFoundationTargetPacks();
1381
+ var DatabricksJobsAdapter = class {
1382
+ constructor(client) {
1383
+ this.client = client;
1384
+ }
1385
+ client;
1386
+ async submit(request) {
1387
+ validateJobGraph(request.tasks);
1388
+ const response = await this.client.post(
1389
+ "/api/2.1/jobs/runs/submit",
1390
+ request
1391
+ );
1392
+ if (!response.run_id) throw new Error("Databricks Jobs submit returned no run_id");
1393
+ return response.run_id;
1394
+ }
1395
+ async get(runId) {
1396
+ const raw = await this.client.get("/api/2.1/jobs/runs/get", {
1397
+ run_id: runId,
1398
+ include_history: true,
1399
+ include_resolved_values: true
1400
+ });
1401
+ return normalizeJobRun(runId, raw);
1402
+ }
1403
+ async wait(runId, options = {}) {
1404
+ const raw = await waitForDatabricksRun(this.client, runId, {
1405
+ timeoutMs: options.timeoutMs,
1406
+ pollIntervalMs: options.pollIntervalMs
1407
+ });
1408
+ return normalizeJobRun(runId, raw);
1409
+ }
1410
+ async cancel(runId) {
1411
+ await this.client.post("/api/2.1/jobs/runs/cancel", { run_id: runId });
1412
+ }
1413
+ async waitUntilActive(runId, options = {}) {
1414
+ const timeoutMs = options.timeoutMs ?? 3e5;
1415
+ const pollIntervalMs = options.pollIntervalMs ?? 2e3;
1416
+ const deadline = Date.now() + timeoutMs;
1417
+ while (Date.now() < deadline) {
1418
+ const run = await this.get(runId);
1419
+ if (run.lifeCycleState === "RUNNING") return run;
1420
+ if (isTerminal(run.lifeCycleState)) {
1421
+ throw new Error(
1422
+ `Databricks run ${runId} reached ${run.lifeCycleState}/${run.resultState} before it could be canceled`
1423
+ );
1424
+ }
1425
+ await delay2(pollIntervalMs);
1426
+ }
1427
+ throw new Error(`Databricks run ${runId} did not become active within ${timeoutMs}ms`);
1428
+ }
1429
+ async repair(runId, options = {}) {
1430
+ const response = await this.client.post("/api/2.1/jobs/runs/repair", {
1431
+ run_id: runId,
1432
+ ...options.latestRepairId ? { latest_repair_id: options.latestRepairId } : {},
1433
+ ...options.rerunDependentTasks ? { rerun_dependent_tasks: true } : {},
1434
+ ...options.rerunTasks?.length ? { rerun_tasks: options.rerunTasks } : { rerun_all_failed_tasks: true }
1435
+ });
1436
+ if (!response.repair_id) throw new Error("Databricks Jobs repair returned no repair_id");
1437
+ return response.repair_id;
1438
+ }
1439
+ async waitForRepair(runId, repairId, options = {}) {
1440
+ const timeoutMs = options.timeoutMs ?? 9e5;
1441
+ const pollIntervalMs = options.pollIntervalMs ?? 5e3;
1442
+ const deadline = Date.now() + timeoutMs;
1443
+ while (Date.now() < deadline) {
1444
+ const run = await this.get(runId);
1445
+ const repair = run.repairs.find((candidate) => candidate.repairId === repairId);
1446
+ if (repair && isTerminal(repair.lifeCycleState)) return run;
1447
+ await delay2(pollIntervalMs);
1448
+ }
1449
+ throw new Error(
1450
+ `Databricks repair ${repairId} for run ${runId} timed out after ${timeoutMs}ms`
1451
+ );
1452
+ }
1453
+ };
1454
+ function validateJobGraph(tasks) {
1455
+ if (tasks.length === 0) throw new Error("Jobs orchestration requires at least one task");
1456
+ const keys = /* @__PURE__ */ new Set();
1457
+ for (const task of tasks) {
1458
+ if (!/^[A-Za-z0-9_-]{1,100}$/.test(task.task_key)) {
1459
+ throw new Error(`Invalid task key '${task.task_key}'`);
1460
+ }
1461
+ if (keys.has(task.task_key)) throw new Error(`Duplicate task key '${task.task_key}'`);
1462
+ keys.add(task.task_key);
1463
+ }
1464
+ const edges = /* @__PURE__ */ new Map();
1465
+ for (const task of tasks) {
1466
+ const dependencies = (task.depends_on ?? []).map((dependency) => dependency.task_key);
1467
+ for (const dependency of dependencies) {
1468
+ if (!keys.has(dependency)) {
1469
+ throw new Error(`Task '${task.task_key}' depends on unknown task '${dependency}'`);
1470
+ }
1471
+ if (dependency === task.task_key) {
1472
+ throw new Error(`Task '${task.task_key}' cannot depend on itself`);
1473
+ }
1474
+ }
1475
+ edges.set(task.task_key, dependencies);
1476
+ }
1477
+ const visiting = /* @__PURE__ */ new Set();
1478
+ const visited = /* @__PURE__ */ new Set();
1479
+ const visit = (key) => {
1480
+ if (visiting.has(key)) throw new Error(`Jobs task graph contains a cycle at '${key}'`);
1481
+ if (visited.has(key)) return;
1482
+ visiting.add(key);
1483
+ for (const dependency of edges.get(key) ?? []) visit(dependency);
1484
+ visiting.delete(key);
1485
+ visited.add(key);
1486
+ };
1487
+ for (const key of keys) visit(key);
1488
+ }
1489
+ function jobsOrchestrationCheck() {
1490
+ return {
1491
+ id: "jobs-orchestration",
1492
+ description: "Lakeflow Jobs multi-task orchestration reaches expected task outcomes",
1493
+ configured: (env) => Boolean(env.DBX_TEST_JOBS_ORCHESTRATION_JSON),
1494
+ async run({ env, client }) {
1495
+ const configured = env.DBX_TEST_JOBS_ORCHESTRATION_JSON;
1496
+ if (!configured) throw new Error("DBX_TEST_JOBS_ORCHESTRATION_JSON is required");
1497
+ const spec = parseJobOrchestrationLiveSpec(configured);
1498
+ const adapter = new DatabricksJobsAdapter(client);
1499
+ const runId = await adapter.submit(spec.request);
1500
+ let run = await adapter.wait(runId, {
1501
+ timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
1502
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 1e4)
1503
+ });
1504
+ let repairId;
1505
+ if (spec.repairFailedTasks && run.resultState !== "SUCCESS") {
1506
+ const failed = run.tasks.filter((task) => task.resultState === "FAILED").map((task) => task.taskKey);
1507
+ repairId = await adapter.repair(runId, {
1508
+ rerunTasks: failed.length > 0 ? failed : void 0,
1509
+ rerunDependentTasks: true
1510
+ });
1511
+ run = await adapter.waitForRepair(runId, repairId, {
1512
+ timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
1513
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 1e4)
1514
+ });
1515
+ }
1516
+ const expectedResult = spec.expectedResult ?? "SUCCESS";
1517
+ if (run.resultState !== expectedResult) {
1518
+ throw new Error(
1519
+ `Job orchestration run ${runId} ended in ${run.resultState}, expected ${expectedResult}`
1520
+ );
1521
+ }
1522
+ for (const [taskKey, expected] of Object.entries(spec.expectedTasks ?? {})) {
1523
+ const task = run.tasks.find((candidate) => candidate.taskKey === taskKey);
1524
+ if (!task) throw new Error(`Job orchestration result did not include task '${taskKey}'`);
1525
+ if (task.resultState !== expected) {
1526
+ throw new Error(`Task '${taskKey}' ended in ${task.resultState}, expected ${expected}`);
1527
+ }
1528
+ }
1529
+ let cancellation;
1530
+ if (spec.cancellation) {
1531
+ const cancellationRunId = await adapter.submit(spec.cancellation.request);
1532
+ await adapter.waitUntilActive(cancellationRunId, {
1533
+ timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
1534
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 1e4)
1535
+ });
1536
+ await adapter.cancel(cancellationRunId);
1537
+ const canceled = await adapter.wait(cancellationRunId, {
1538
+ timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
1539
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 1e4)
1540
+ });
1541
+ const expectedCancellation = spec.cancellation.expectedResult ?? "CANCELED";
1542
+ if (canceled.resultState !== expectedCancellation) {
1543
+ throw new Error(
1544
+ `Cancellation run ${cancellationRunId} ended in ${canceled.resultState}, expected ${expectedCancellation}`
1545
+ );
1546
+ }
1547
+ cancellation = { runId: cancellationRunId, resultState: canceled.resultState };
1548
+ }
1549
+ return {
1550
+ runId,
1551
+ ...repairId ? { repairId } : {},
1552
+ resultState: run.resultState,
1553
+ tasks: run.tasks,
1554
+ ...cancellation ? { cancellation } : {}
1555
+ };
1556
+ }
1557
+ };
1558
+ }
1559
+ function parseJobOrchestrationLiveSpec(value) {
1560
+ let parsed;
1561
+ try {
1562
+ parsed = JSON.parse(value);
1563
+ } catch (error) {
1564
+ throw new Error(`DBX_TEST_JOBS_ORCHESTRATION_JSON is invalid JSON: ${String(error)}`);
1565
+ }
1566
+ if (!parsed || typeof parsed !== "object") {
1567
+ throw new Error("Jobs orchestration spec must be an object");
1568
+ }
1569
+ const spec = parsed;
1570
+ if (!spec.request || typeof spec.request !== "object" || !Array.isArray(spec.request.tasks)) {
1571
+ throw new Error("Jobs orchestration spec requires request.tasks");
1572
+ }
1573
+ validateJobGraph(spec.request.tasks);
1574
+ if (spec.cancellation) {
1575
+ if (typeof spec.cancellation !== "object" || !spec.cancellation.request || !Array.isArray(spec.cancellation.request.tasks)) {
1576
+ throw new Error("Jobs orchestration cancellation requires request.tasks");
1577
+ }
1578
+ validateJobGraph(spec.cancellation.request.tasks);
1579
+ }
1580
+ return spec;
1581
+ }
1582
+ function normalizeJobRun(fallbackRunId, raw) {
1583
+ const state = objectOf(raw.state);
1584
+ const repairEntries = Array.isArray(raw.repair_history) ? raw.repair_history.filter(
1585
+ (repair) => Boolean(repair && typeof repair === "object")
1586
+ ) : [];
1587
+ const taskRunRanks = /* @__PURE__ */ new Map();
1588
+ for (const [rank, repair] of repairEntries.entries()) {
1589
+ for (const runId of numberArray(repair.task_run_ids)) taskRunRanks.set(runId, rank);
1590
+ }
1591
+ const taskAttempts = Array.isArray(raw.tasks) ? raw.tasks.filter(
1592
+ (task) => Boolean(task && typeof task === "object")
1593
+ ).map((task) => {
1594
+ const taskState = objectOf(task.state);
1595
+ return {
1596
+ taskKey: String(task.task_key ?? "unknown"),
1597
+ ...Number.isFinite(Number(task.run_id)) ? { runId: Number(task.run_id) } : {},
1598
+ lifeCycleState: String(taskState.life_cycle_state ?? "UNKNOWN"),
1599
+ resultState: String(taskState.result_state ?? "UNKNOWN"),
1600
+ ...typeof taskState.state_message === "string" ? { stateMessage: taskState.state_message } : {},
1601
+ ...Number.isFinite(Number(task.attempt_number)) ? { attemptNumber: Number(task.attempt_number) } : {}
1602
+ };
1603
+ }) : [];
1604
+ const latestTasks = /* @__PURE__ */ new Map();
1605
+ for (const task of taskAttempts) {
1606
+ const current = latestTasks.get(task.taskKey);
1607
+ const currentAttempt = current?.attemptNumber ?? -1;
1608
+ const candidateAttempt = task.attemptNumber ?? -1;
1609
+ const currentRank = current?.runId ? taskRunRanks.get(current.runId) ?? -1 : -1;
1610
+ const candidateRank = task.runId ? taskRunRanks.get(task.runId) ?? -1 : -1;
1611
+ if (!current || candidateAttempt > currentAttempt || candidateAttempt === currentAttempt && candidateRank >= currentRank) {
1612
+ latestTasks.set(task.taskKey, task);
1613
+ }
1614
+ }
1615
+ const tasks = [...latestTasks.values()];
1616
+ const repairs = repairEntries.map((repair) => {
1617
+ const repairState = objectOf(repair.state);
1618
+ return {
1619
+ repairId: Number(repair.id ?? repair.repair_id ?? 0),
1620
+ type: String(repair.type ?? "UNKNOWN"),
1621
+ lifeCycleState: String(repairState.life_cycle_state ?? "UNKNOWN"),
1622
+ resultState: String(repairState.result_state ?? "UNKNOWN"),
1623
+ taskRunIds: numberArray(repair.task_run_ids)
1624
+ };
1625
+ });
1626
+ return {
1627
+ runId: Number(raw.run_id ?? fallbackRunId),
1628
+ lifeCycleState: String(state.life_cycle_state ?? "UNKNOWN"),
1629
+ resultState: String(state.result_state ?? "UNKNOWN"),
1630
+ tasks,
1631
+ repairs,
1632
+ raw
1633
+ };
1634
+ }
1635
+ function isTerminal(state) {
1636
+ return ["TERMINATED", "SKIPPED", "INTERNAL_ERROR", "BLOCKED"].includes(state);
1637
+ }
1638
+ function delay2(ms) {
1639
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1640
+ }
1641
+ function objectOf(value) {
1642
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1643
+ }
1644
+ function numberArray(value) {
1645
+ return Array.isArray(value) ? value.map(Number).filter((candidate) => Number.isFinite(candidate)) : [];
1646
+ }
1647
+
1648
+ // src/jobs-certification-check.ts
1649
+ var NOTEBOOKS = {
1650
+ seed: `# Databricks notebook source
1651
+ dbutils.jobs.taskValues.set(key="branch", value="run")
1652
+ dbutils.jobs.taskValues.set(key="items", value=["alpha", "beta"])
1653
+ dbutils.notebook.exit("seeded")
1654
+ `,
1655
+ process: `# Databricks notebook source
1656
+ dbutils.widgets.text("item", "")
1657
+ item = dbutils.widgets.get("item")
1658
+ assert item, "item parameter is required"
1659
+ dbutils.notebook.exit(item)
1660
+ `,
1661
+ retry: `# Databricks notebook source
1662
+ dbutils.widgets.text("execution_count", "0")
1663
+ execution_count = int(dbutils.widgets.get("execution_count"))
1664
+ if execution_count <= 1:
1665
+ raise RuntimeError("intentional retryable failure")
1666
+ dbutils.notebook.exit("retried")
1667
+ `,
1668
+ repair: `# Databricks notebook source
1669
+ dbutils.widgets.text("repair_count", "0")
1670
+ repair_count = int(dbutils.widgets.get("repair_count"))
1671
+ if repair_count == 0:
1672
+ raise RuntimeError("intentional first-run failure")
1673
+ dbutils.notebook.exit("repaired")
1674
+ `,
1675
+ sleep: `# Databricks notebook source
1676
+ import time
1677
+ time.sleep(300)
1678
+ `
1679
+ };
1680
+ function jobsCertificationCheck() {
1681
+ return {
1682
+ id: "jobs-certification",
1683
+ description: "Disposable Lakeflow Jobs branch, loop, failure, repair, cancellation, and cleanup certification",
1684
+ configured: (env) => env.DBX_TEST_JOBS_CERTIFY === "1",
1685
+ async run({ env, client }) {
1686
+ const root = `${env.DBX_TEST_JOBS_FIXTURE_ROOT ?? "/Workspace/Shared/fabric-experiments-target-pack"}/${randomUUID()}`;
1687
+ const paths = Object.fromEntries(
1688
+ Object.keys(NOTEBOOKS).map((name) => [name, `${root}/${name}`])
1689
+ );
1690
+ try {
1691
+ await importNotebooks(client, paths);
1692
+ const adapter = new DatabricksJobsAdapter(client);
1693
+ const runId = await adapter.submit(certificationRequest(paths));
1694
+ const initial = await adapter.wait(runId, waitOptions(env));
1695
+ if (initial.resultState !== "FAILED") {
1696
+ throw new Error(
1697
+ `Certification run ${runId} expected FAILED before repair, got ${initial.resultState}`
1698
+ );
1699
+ }
1700
+ const retried = assertTask(initial.tasks, "retry", "SUCCESS");
1701
+ if ((retried.attemptNumber ?? 0) < 1) {
1702
+ throw new Error("Certification retry task did not record a second attempt");
1703
+ }
1704
+ assertTask(initial.tasks, "seed", "SUCCESS");
1705
+ assertTask(initial.tasks, "branch", "SUCCESS");
1706
+ assertTask(initial.tasks, "foreach", "SUCCESS");
1707
+ assertTask(initial.tasks, "repair", "FAILED");
1708
+ const repairId = await adapter.repair(runId, {
1709
+ rerunTasks: ["repair"],
1710
+ rerunDependentTasks: true
1711
+ });
1712
+ const repaired = await adapter.waitForRepair(runId, repairId, waitOptions(env));
1713
+ if (repaired.resultState !== "SUCCESS") {
1714
+ throw new Error(`Certification repair ${repairId} ended in ${repaired.resultState}`);
1715
+ }
1716
+ assertTask(repaired.tasks, "repair", "SUCCESS");
1717
+ const cancellationRunId = await adapter.submit(cancellationRequest(paths));
1718
+ await adapter.waitUntilActive(cancellationRunId, waitOptions(env));
1719
+ await adapter.cancel(cancellationRunId);
1720
+ const canceled = await adapter.wait(cancellationRunId, waitOptions(env));
1721
+ if (canceled.resultState !== "CANCELED") {
1722
+ throw new Error(`Certification cancellation ended in ${canceled.resultState}`);
1723
+ }
1724
+ return {
1725
+ runId,
1726
+ repairId,
1727
+ cancellationRunId,
1728
+ taskValues: true,
1729
+ retry: true,
1730
+ condition: true,
1731
+ forEach: true,
1732
+ repair: true,
1733
+ cancellation: true,
1734
+ fixtureRoot: root
1735
+ };
1736
+ } finally {
1737
+ await client.post("/api/2.0/workspace/delete", { path: root, recursive: true });
1738
+ }
1739
+ }
1740
+ };
1741
+ }
1742
+ async function importNotebooks(client, paths) {
1743
+ await client.post("/api/2.0/workspace/mkdirs", {
1744
+ path: paths.seed.slice(0, paths.seed.lastIndexOf("/"))
1745
+ });
1746
+ for (const [name, source] of Object.entries(NOTEBOOKS)) {
1747
+ await client.post("/api/2.0/workspace/import", {
1748
+ path: paths[name],
1749
+ format: "SOURCE",
1750
+ language: "PYTHON",
1751
+ overwrite: true,
1752
+ content: Buffer.from(source).toString("base64")
1753
+ });
1754
+ }
1755
+ }
1756
+ function certificationRequest(paths) {
1757
+ return {
1758
+ run_name: "fabric-experiments-jobs-certification",
1759
+ tasks: [
1760
+ {
1761
+ task_key: "retry",
1762
+ notebook_task: {
1763
+ notebook_path: paths.retry,
1764
+ base_parameters: { execution_count: "{{task.execution_count}}" }
1765
+ },
1766
+ max_retries: 1
1767
+ },
1768
+ {
1769
+ task_key: "seed",
1770
+ depends_on: [{ task_key: "retry" }],
1771
+ notebook_task: { notebook_path: paths.seed }
1772
+ },
1773
+ {
1774
+ task_key: "branch",
1775
+ depends_on: [{ task_key: "seed" }],
1776
+ condition_task: {
1777
+ left: "{{tasks.seed.values.branch}}",
1778
+ op: "EQUAL_TO",
1779
+ right: "run"
1780
+ }
1781
+ },
1782
+ {
1783
+ task_key: "foreach",
1784
+ depends_on: [{ task_key: "branch", outcome: "true" }],
1785
+ for_each_task: {
1786
+ inputs: "{{tasks.seed.values.items}}",
1787
+ concurrency: 2,
1788
+ task: {
1789
+ task_key: "process_item",
1790
+ notebook_task: {
1791
+ notebook_path: paths.process,
1792
+ base_parameters: { item: "{{input}}" }
1793
+ }
1794
+ }
1795
+ }
1796
+ },
1797
+ {
1798
+ task_key: "repair",
1799
+ depends_on: [{ task_key: "foreach" }],
1800
+ notebook_task: {
1801
+ notebook_path: paths.repair,
1802
+ base_parameters: { repair_count: "{{job.repair_count}}" }
1803
+ }
1804
+ }
1805
+ ]
1806
+ };
1807
+ }
1808
+ function cancellationRequest(paths) {
1809
+ return {
1810
+ run_name: "fabric-experiments-jobs-cancellation-certification",
1811
+ tasks: [{ task_key: "sleep", notebook_task: { notebook_path: paths.sleep } }]
1812
+ };
1813
+ }
1814
+ function waitOptions(env) {
1815
+ return {
1816
+ timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
1817
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 5e3)
1818
+ };
1819
+ }
1820
+ function assertTask(tasks, taskKey, resultState) {
1821
+ const task = tasks.find((candidate) => candidate.taskKey === taskKey);
1822
+ if (!task) throw new Error(`Certification result did not contain task '${taskKey}'`);
1823
+ if (task.resultState !== resultState) {
1824
+ throw new Error(
1825
+ `Certification task '${taskKey}' ended in ${task.resultState}, expected ${resultState}`
1826
+ );
1827
+ }
1828
+ return task;
1829
+ }
1830
+
1831
+ // src/jobs-target-pack.ts
1832
+ function createLakeflowJobsTargetPack() {
1833
+ return defineTargetPack({
1834
+ id: "lakeflow-jobs",
1835
+ name: "Lakeflow Jobs orchestration",
1836
+ description: "Typed multi-task DAG, branch/loop, retry, cancellation, repair and task-outcome validation.",
1837
+ version: "0.2.0",
1838
+ maturity: "live-gated",
1839
+ capabilities: [
1840
+ "jobs.multi-task-dag",
1841
+ "jobs.dependencies",
1842
+ "jobs.condition-task",
1843
+ "jobs.for-each-task",
1844
+ "jobs.retry",
1845
+ "jobs.cancel",
1846
+ "jobs.repair",
1847
+ "jobs.task-outcomes"
1848
+ ],
1849
+ checks: () => [
1850
+ restrictedPrincipalCheck(),
1851
+ jobRunCheck(),
1852
+ jobsOrchestrationCheck(),
1853
+ jobsCertificationCheck()
1854
+ ],
1855
+ requiredChecks: ["jobs-orchestration"],
1856
+ requirements: [
1857
+ {
1858
+ id: "workspace",
1859
+ description: "Databricks workspace host",
1860
+ anyOf: ["DATABRICKS_HOST"]
1861
+ },
1862
+ {
1863
+ id: "authentication",
1864
+ description: "Databricks OAuth, OIDC, bearer, or PAT authentication",
1865
+ anyOf: [
1866
+ "DATABRICKS_BEARER",
1867
+ "DATABRICKS_TOKEN",
1868
+ "DATABRICKS_CLIENT_ID",
1869
+ "DATABRICKS_OIDC_TOKEN",
1870
+ "DATABRICKS_OIDC_TOKEN_FILEPATH"
1871
+ ],
1872
+ secret: true
1873
+ },
1874
+ {
1875
+ id: "orchestration-spec",
1876
+ description: "JSON multi-task orchestration scenario",
1877
+ anyOf: ["DBX_TEST_JOBS_ORCHESTRATION_JSON"],
1878
+ required: false
1879
+ }
1880
+ ],
1881
+ docsUrl: "https://experiments.fabric.pro/docs/testing/target-packs/#lakeflow-jobs-orchestration",
1882
+ certificationScopes: [
1883
+ "Azure Databricks serverless Jobs, West US, workspace OAuth, public network path"
1884
+ ]
1885
+ });
1886
+ }
1887
+
1888
+ // src/builtin-packs.ts
1889
+ function createBuiltinTargetPacks() {
1890
+ return [...createFoundationTargetPacks(), createLakeflowJobsTargetPack()];
1891
+ }
1892
+ var builtinTargetPacks = createBuiltinTargetPacks();
1893
+
1894
+ export { DatabricksJobsAdapter, appHealthCheck, assertDeltaContract, autoLoaderIngestionCheck, backupRestoreCheck, builtinTargetPacks, classicComputeCheck, createAppsOperationalFoundationPack, createBuiltinTargetPacks, createEphemeralLakebaseBranch, createExecutionContext, createFoundationTargetPacks, createJobsCodeFoundationPack, createLakebaseTestProvider, createLakeflowFoundationPack, createLakeflowJobsTargetPack, createMockDatabricksClient, createSqlDeltaFoundationPack, createTargetPackRegistry, createUnityStorageFoundationPack, customLiveCheck, dbtBuildCheck, defineLiveSuite, defineTargetPack, defineTargetPackRun, deleteEphemeralLakebaseBranch, deleteVolumeFile, deltaContractCheck, doctorTargetPack, ensureVolumeDirectory, expectQueryToMatchGolden, expectTable, failureRecoveryCheck, foundationTargetPacks, jobRunCheck, jobsCertificationCheck, jobsOrchestrationCheck, lakebaseCredentialCheck, lakebaseRoundTripCheck, normalizeJobRun, normalizeVolumeRoot, notebookSubmitCheck, parseJobOrchestrationLiveSpec, performanceBudgetCheck, pipelineRefreshCheck, renderJUnit, resolveLakebaseProject, resolveProfile, restrictedPrincipalCheck, rollbackCheck, runLiveSuite, runTargetPack, secretRotationCheck, secretScopeCheck, serverlessComputeCheck, sqlRoundTripCheck, toLakebaseBranchId, unityCatalogAccessCheck, uploadVolumeFile, validateJobGraph, volumeIOCheck, waitForPipelineUpdate };
1076
1895
  //# sourceMappingURL=index.js.map
1077
1896
  //# sourceMappingURL=index.js.map