Restoring Corrupted ATF Step Order: A Scripted Recovery Strategy
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
yesterday
You may've seen this, a glitch causes an Automated Test Framework (ATF) test to lose its step ordering. Suddenly, Step 5 is Step 1, or you have two Step 22s etc and the test is unusable. Manually reordering dozens of steps is tedious and prone to human error.
https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0861763
Rather than accepting "This is expected behavior and by design in all currently supported releases." and using the provided remedy...
- To remove duplicate 'Execution order', Use the 'Copy Test' button and save the copied test.
- To remove any duplicate steps, select the step then using the 'action on selected rows...' choose delete.
- Check the flow of the test and reorder as required.
...we can take a far simpler approach.
Instead of guessing the order, I wrote a background script to recover the correct sequence from the last successful execution of the test. By using the sys_atf_test_result table as the "source of truth," we can mathematically guarantee the steps are restored to their working order.
The script operates in five distinct phases to ensure data integrity:
- It locates the most recent "Success" result for the target test and maps the historical execution order.
- It performs a "Strict Match" check. It compares the step count and Sys IDs of the historical run against the current test definition. If the test has been modified (steps added/removed) since the last run, the script aborts to prevent damage.
- It sets the order field of all current steps to NULL.
- It applies the recorded order from the history. Crucially, it enables workflows (setWorkflow(true)) during this phase so the changes are captured in your active Update Set.
- It reads the data back one last time to ensure the platform hasn't auto-sorted the steps incorrectly.
The Script Note: Please run this in a Sub-Production environment first. You must replace REPLACE_WITH_SYSID_OF_TEST with the Sys ID of the corrupted test.
(function() {
// ------------------------------------------------------------------
// CONFIGURATION
// ------------------------------------------------------------------
var TARGET_TEST_SYS_ID = 'REPLACE_WITH_SYSID_OF_TEST';
// ------------------------------------------------------------------
// 1. FETCH: Find the last successful run
// ------------------------------------------------------------------
gs.info('--- PHASE 1: SEARCHING FOR LAST SUCCESSFUL RUN ---');
var resultGr = new GlideRecord('sys_atf_test_result');
resultGr.addQuery('test', TARGET_TEST_SYS_ID);
resultGr.addQuery('status', 'success');
resultGr.orderByDesc('sys_created_on');
resultGr.setLimit(1);
resultGr.query();
if (!resultGr.next()) {
gs.error('ABORTING: No successful test result found for Test ID: ' + TARGET_TEST_SYS_ID);
return;
}
gs.info('Found Reference Run: ' + resultGr.getDisplayValue() + ' (' + resultGr.sys_created_on + ')');
// ------------------------------------------------------------------
// 2. MAP & VALIDATE: Compare History vs. Current Reality
// ------------------------------------------------------------------
gs.info('--- PHASE 2: VALIDATING INTEGRITY ---');
var historyMap = {};
var historyCount = 0;
// Query the RESULT steps using the confirmed 'order' field
var resultStepGr = new GlideRecord('sys_atf_test_result_step');
resultStepGr.addQuery('test_result', resultGr.getUniqueValue());
resultStepGr.orderBy('order');
resultStepGr.query();
while (resultStepGr.next()) {
var stepId = resultStepGr.step.toString();
if (stepId) {
// Store order as integer for accurate comparison later
historyMap[stepId] = parseInt(resultStepGr.getValue('order'), 10);
historyCount++;
}
}
gs.info('Mapped ' + historyCount + ' steps from history.');
// B. Analyze Current Test Definition
var currentStepGr = new GlideRecord('sys_atf_step');
currentStepGr.addQuery('test', TARGET_TEST_SYS_ID);
currentStepGr.query();
var currentCount = currentStepGr.getRowCount();
var matchedCount = 0;
// CHECK 1: Count Mismatch
if (historyCount !== currentCount) {
gs.error('ABORTING: Count Mismatch. Historical run had ' + historyCount + ' steps, but current test has ' + currentCount + '.');
return;
}
// CHECK 2 & 3: ID Match & Active State
while (currentStepGr.next()) {
var currentId = currentStepGr.getUniqueValue();
if (!historyMap.hasOwnProperty(currentId)) {
gs.error('ABORTING: Unknown Step. Step "' + currentStepGr.step_config.getDisplayValue() + '" exists now but was not in the reference run.');
return;
}
if (currentStepGr.active == false) {
gs.error('ABORTING: Inactive Step. Step "' + currentStepGr.step_config.getDisplayValue() + '" matches history but is currently Inactive.');
return;
}
matchedCount++;
}
if (matchedCount !== historyCount) {
gs.error('ABORTING: Validation Logic Error. Matched count (' + matchedCount + ') does not equal history count (' + historyCount + ').');
return;
}
gs.info('VALIDATION PASSED: Current test structure matches historical execution.');
// ------------------------------------------------------------------
// 3. FLUSH: Nullify Orders
// ------------------------------------------------------------------
gs.info('--- PHASE 3: NULLIFYING EXISTING ORDERS ---');
var flushGr = new GlideRecord('sys_atf_step');
flushGr.addQuery('test', TARGET_TEST_SYS_ID);
flushGr.orderBy('order');
flushGr.query();
while (flushGr.next()) {
gs.info('Step ' + flushGr.order + ' ' + flushGr.step_config.getDisplayValue() + ' set as NULL.');
flushGr.setValue('order', null);
flushGr.update();
gs.info('Read order back - ' + flushGr.getValue('order'));
}
gs.info('Existing step orders set to NULL.');
// ------------------------------------------------------------------
// 4. FIX: Apply Recorded Order (WITH WORKFLOWS)
// ------------------------------------------------------------------
gs.info('--- PHASE 4: RESTORING HISTORICAL ORDER (CAPTURING IN UPDATE SET) ---');
for (var id in historyMap) {
var restoreGr = new GlideRecord('sys_atf_step');
if (restoreGr.get(id)) {
restoreGr.order = historyMap[id];
restoreGr.setWorkflow(true);
restoreGr.update();
}
}
// ------------------------------------------------------------------
// 5. VERIFY: Compare Final State vs History Map
// ------------------------------------------------------------------
gs.info('--- PHASE 5: STRICT VERIFICATION ---');
var verifyGr = new GlideRecord('sys_atf_step');
verifyGr.addQuery('test', TARGET_TEST_SYS_ID);
verifyGr.orderBy('order');
verifyGr.query();
var verificationFailures = 0;
while (verifyGr.next()) {
var verifyId = verifyGr.getUniqueValue();
var actualOrder = parseInt(verifyGr.getValue('order'), 10);
var expectedOrder = historyMap[verifyId];
if (actualOrder !== expectedOrder) {
gs.error('FAILURE: Step "' + verifyGr.step_config.getDisplayValue() + '" has Order ' + actualOrder + ' but expected ' + expectedOrder);
verificationFailures++;
} else {
gs.info('MATCH: Order ' + actualOrder + ' confirmed for step ' + verifyGr.step_config.getDisplayValue());
}
}
if (verificationFailures > 0) {
gs.error('--- RESTORATION FAILED WITH ' + verificationFailures + ' ERRORS ---');
} else {
gs.info('--- RESTORATION SUCCESSFUL: All steps match historical order ---');
}
})();