import { CallExpressionShape, deleteMultipleDiff, isGUID, Plugin, RecordId, type Diagnostics, type Factory, type Record as RecordShape, type Shape, } from '@servicenow/sdk-build-core' import { NowIdShape } from '../now-id-plugin' async function resolveSuiteOrTestRef( shape: Shape, table: 'sys_atf_test_suite' | 'sys_atf_test', factory: Factory, diagnostics: Diagnostics ): Promise { if (!shape.isDefined()) { return undefined } if (shape.isStringLiteral()) { const value = shape.getValue() if (!isGUID(value)) { diagnostics.error(shape, `'${value}' is not a valid sys_id.`) return undefined } return factory.createReference({ source: shape, table, guid: shape }) } return shape.ifRecord()?.getId() ?? shape } export const TestSuitePlugin = Plugin.create({ name: 'TestSuitePlugin', records: { sys_atf_test_suite: { relationships: { sys_atf_test_suite_test: { via: 'test_suite', descendant: true, }, }, toShape(record, { descendants }) { const joinRows = descendants .query('sys_atf_test_suite_test') .sort((a, b) => a.get('order').toNumber().getValue() - b.get('order').toNumber().getValue()) const tests = joinRows.map((row, index) => { const testRef = row.get('test').ifString()?.getValue() ?? '' const abortOnFailure = row.get('abort_on_failure').toBoolean().getValue() const rawOrder = row.get('order').toNumber().getValue() const order = Number.isFinite(rawOrder) ? rawOrder : index + 1 const isSequential = order === index + 1 if (isSequential && !abortOnFailure) { return testRef } const entry: { test: string; abortOnFailure?: boolean; order?: number } = { test: testRef } if (abortOnFailure) { entry.abortOnFailure = true } if (!isSequential) { entry.order = order } return entry }) return { success: true, value: new CallExpressionShape({ source: record, exportName: record.get('name').ifString()?.getValue(), callee: 'TestSuite', args: [ record.transform(({ $ }) => ({ $id: $.val(NowIdShape.from(record)), name: $, active: $.toBoolean().def(true), description: $.def(''), parent: $.from('parent').def(''), tests: $.val(tests).def([]), testFilter: $.from('input_filter').def(''), })), ], }), } }, async diff(existing, incoming, descendants, context) { return deleteMultipleDiff(existing, incoming, descendants, context) }, }, sys_atf_test_suite_test: { coalesce: ['test_suite', 'test'], }, }, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { factory, diagnostics }) { if (callExpression.getCallee() !== 'TestSuite') { return { success: false } } const arg = callExpression.getArgument(0).asObject() const name = arg.get('name').ifString()?.getValue() ?? '' if (name.trim() === '') { diagnostics.error(arg.get('name'), `'name' must not be empty.`) } const parentShape = arg.get('parent') const parentValue = await resolveSuiteOrTestRef(parentShape, 'sys_atf_test_suite', factory, diagnostics) const testFilter = arg.get('testFilter').ifString()?.getValue() ?? '' const hasExplicitTests = (arg.get('tests').ifArray()?.getElements().length ?? 0) > 0 if (testFilter.trim() !== '' && hasExplicitTests) { diagnostics.hint( arg.get('testFilter'), `This suite also lists explicit 'tests'. ATF's filter-driven membership sync is ` + `triggered by a business rule on save, not by this install/build — the next time ` + `the suite record is saved on the instance, membership rows may be added or ` + `removed to match 'testFilter', which may override the explicit list.` ) } const suiteRecord = await factory.createRecord({ source: callExpression, table: 'sys_atf_test_suite', explicitId: arg.get('$id'), properties: arg.transform(({ $ }) => ({ name: $, active: $.def(true), description: $.def(''), parent: $.val(parentValue).def(''), input_filter: $.val(testFilter), })), }) if (parentValue instanceof RecordId && parentValue.getValue() === suiteRecord.getId().getValue()) { diagnostics.error(parentShape, `A test suite cannot be its own parent.`) } const entries = arg.get('tests').ifArray()?.getElements() ?? [] const seenTestIds = new Set() const seenOrders = new Set() const joinRecords: RecordShape[] = [] let previousOrder = 0 for (let index = 0; index < entries.length; index++) { const entry = entries[index]! const entryAsObject = entry.ifObject() const hasTestProperty = entryAsObject?.has('test') ?? false const testShape = hasTestProperty ? entryAsObject!.get('test') : entry const abortOnFailure = hasTestProperty ? (entryAsObject!.get('abortOnFailure').ifBoolean()?.getValue() ?? false) : false const hasExplicitOrder = entryAsObject?.has('order') ?? false const explicitOrder = hasExplicitOrder ? entryAsObject!.get('order').ifNumber()?.getValue() : undefined const order = explicitOrder ?? previousOrder + 1 if (seenOrders.has(order)) { diagnostics.hint( hasExplicitOrder ? entryAsObject!.get('order') : entry, `Multiple tests in this suite resolve to order '${order}'. ATF doesn't guarantee ` + `run order between ties.` ) } else if (explicitOrder !== undefined && explicitOrder <= previousOrder && index > 0) { diagnostics.hint( entryAsObject!.get('order'), `This test's pinned order '${explicitOrder}' is not after the preceding entry's ` + `order '${previousOrder}' — its position in 'tests' and its pinned 'order' ` + `disagree about run order.` ) } seenOrders.add(order) previousOrder = order const testValue = await resolveSuiteOrTestRef(testShape, 'sys_atf_test', factory, diagnostics) if (testValue === undefined) { continue } if (testValue instanceof RecordId) { const resolvedId = testValue.getValue() if (seenTestIds.has(resolvedId)) { diagnostics.error( entry, `Test '${resolvedId}' is listed more than once in this suite's 'tests' array.` ) } seenTestIds.add(resolvedId) } joinRecords.push( await factory.createRecord({ source: entry, table: 'sys_atf_test_suite_test', properties: { test_suite: suiteRecord.getId(), test: testValue, order, abort_on_failure: abortOnFailure, }, }) ) } return { success: true, value: suiteRecord.with(...joinRecords) } }, }, ], })