Skip to content

Commit 3e118a9

Browse files
gololdf1shclaude
andcommitted
fix(plugins): resolve async race conditions in aiTrace, analyze, screencast, pageInfo, heal
Fix 8 bugs across 6 plugin files where async operations outside the recorder chain, missing force flags, and incorrect filtering caused silent data loss, premature process exit, and broken healing limits. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0c07a9c commit 3e118a9

6 files changed

Lines changed: 45 additions & 29 deletions

File tree

lib/heal.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ class Heal {
5454

5555
async getCodeSuggestions(context) {
5656
const suggestions = []
57+
const stepName = context.step?.name
5758
const recipes = matchRecipes(this.recipes, this.contextName)
59+
.filter(r => !r.steps || !stepName || r.steps.includes(stepName))
5860

5961
debug('Recipes', recipes)
6062

lib/plugin/aiTrace.js

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export default function (config = {}) {
9292
let testStartTime
9393
let currentUrl = null
9494
let testFailed = false
95+
let pendingArtifactCapture = null
9596
let firstFailedStepSaved = false
9697

9798
const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output
@@ -129,6 +130,7 @@ export default function (config = {}) {
129130
currentUrl = null
130131
testFailed = false
131132
firstFailedStepSaved = false
133+
pendingArtifactCapture = null
132134
})
133135

134136
event.dispatcher.on(event.step.after, step => {
@@ -162,13 +164,12 @@ export default function (config = {}) {
162164
return
163165
}
164166

165-
const stepPersistPromise = persistStep(step).catch(err => {
167+
recorder.add(`aiTrace step persistence: ${step.toString()}`, () => persistStep(step).catch(err => {
166168
output.debug(`aiTrace: Error saving step: ${err.message}`)
167-
})
168-
recorder.add(`wait aiTrace step persistence: ${step.toString()}`, () => stepPersistPromise, true)
169+
}), true)
169170
})
170171

171-
event.dispatcher.on(event.step.failed, async step => {
172+
event.dispatcher.on(event.step.failed, step => {
172173
if (!currentTest) return
173174
if (step.status === 'queued' && testFailed) {
174175
output.debug(`aiTrace: Skipping queued failed step "${step.toString()}" - testFailed: ${testFailed}`)
@@ -188,11 +189,9 @@ export default function (config = {}) {
188189
}
189190
existingStep.status = 'failed'
190191

191-
try {
192-
await captureArtifactsForStep(step, existingStep, existingStep.prefix)
193-
} catch (err) {
192+
pendingArtifactCapture = captureArtifactsForStep(step, existingStep, existingStep.prefix).catch(err => {
194193
output.debug(`aiTrace: Error updating failed step: ${err.message}`)
195-
}
194+
})
196195
} else {
197196
if (stepNum === -1) return
198197
if (isStepIgnored(step)) return
@@ -218,11 +217,9 @@ export default function (config = {}) {
218217
steps.push(stepData)
219218
firstFailedStepSaved = true
220219

221-
try {
222-
await captureArtifactsForStep(step, stepData, stepPrefix)
223-
} catch (err) {
220+
pendingArtifactCapture = captureArtifactsForStep(step, stepData, stepPrefix).catch(err => {
224221
output.debug(`aiTrace: Error capturing failed step artifacts: ${err.message}`)
225-
}
222+
})
226223
}
227224
})
228225

@@ -238,7 +235,13 @@ export default function (config = {}) {
238235
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
239236
return
240237
}
241-
persist(test, 'failed')
238+
recorder.add('aiTrace:persist failed', async () => {
239+
if (pendingArtifactCapture) {
240+
await pendingArtifactCapture
241+
pendingArtifactCapture = null
242+
}
243+
persist(test, 'failed')
244+
}, true)
242245
})
243246

244247
async function persistStep(step) {

lib/plugin/analyze.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,12 @@ export default function (config = {}) {
234234
return
235235
}
236236

237-
printReport(result)
237+
process.env.CODECEPT_DISABLE_AUTO_EXIT = '1'
238+
try {
239+
await printReport(result)
240+
} finally {
241+
process.exit(process.exitCode || 0)
242+
}
238243
})
239244

240245
event.dispatcher.on(event.workers.result, async result => {
@@ -248,7 +253,12 @@ export default function (config = {}) {
248253
return
249254
}
250255

251-
printReport(result)
256+
process.env.CODECEPT_DISABLE_AUTO_EXIT = '1'
257+
try {
258+
await printReport(result)
259+
} finally {
260+
process.exit(process.exitCode || 0)
261+
}
252262
})
253263

254264
async function printReport(result) {
@@ -294,7 +304,7 @@ export default function (config = {}) {
294304
console.error('Error analyzing failed tests', err)
295305
}
296306

297-
if (!Object.keys(container.plugins()).includes('pageInfo')) {
307+
if (!Object.keys(Container.plugins()).includes('pageInfo')) {
298308
console.log('To improve analysis, enable pageInfo plugin to get more context for failed tests.')
299309
}
300310
}

lib/plugin/heal.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export default function (config = {}) {
8080
event.dispatcher.on(event.test.before, test => {
8181
currentTest = test
8282
healedSteps = 0
83+
healTries = 0
8384
caughtError = null
8485
})
8586

@@ -94,7 +95,9 @@ export default function (config = {}) {
9495
if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
9596

9697
recorder.catchWithoutStop(async err => {
98+
if (healTries >= config.healLimit) throw err
9799
isHealing = true
100+
healTries++
98101
if (caughtError === err) throw err // avoid double handling
99102
caughtError = err
100103

@@ -121,8 +124,6 @@ export default function (config = {}) {
121124

122125
await heal.healStep(step, err, { test })
123126

124-
healTries++
125-
126127
recorder.add('close healing session', () => {
127128
recorder.reset()
128129
recorder.session.restore('heal')

lib/plugin/pageInfo.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ const defaultConfig = {
4040
export default function (config = {}) {
4141
config = Object.assign(defaultConfig, config)
4242

43-
const helper = pickActingHelper(Container.helpers())
44-
if (!helper) return
45-
4643
event.dispatcher.on(event.test.failed, test => {
44+
const helper = pickActingHelper(Container.helpers())
45+
if (!helper) return
46+
4747
const pageState = {}
4848

4949
recorder.add('pageInfo capture', async () => {
@@ -60,8 +60,6 @@ export default function (config = {}) {
6060
if (captured.html) {
6161
const htmlPath = path.join(store.outputDir, captured.html)
6262
pageState.htmlSnapshot = htmlPath
63-
// Scan raw HTML (pre-cleanHtml) so error classes containing digits
64-
// or trash-class prefixes aren't stripped before detection.
6563
const htmlForScan = captured.htmlRaw || (() => {
6664
try { return fs.readFileSync(htmlPath, 'utf8') } catch { return '' }
6765
})()
@@ -90,7 +88,7 @@ export default function (config = {}) {
9088
} catch {}
9189
}
9290
} catch {}
93-
})
91+
}, true)
9492

9593
recorder.add('Save page info', () => {
9694
test.addNote('pageInfo', pageStateToMarkdown(pageState))
@@ -99,7 +97,7 @@ export default function (config = {}) {
9997
fs.writeFileSync(pageStateFileName, pageStateToMarkdown(pageState))
10098
test.artifacts.pageInfo = pageStateFileName
10199
return pageState
102-
})
100+
}, true)
103101
})
104102
}
105103

lib/plugin/screencast.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ function wireScreencast(mode, options) {
106106
state.startedAt = options.subtitles ? Date.now() : null
107107
})
108108

109+
event.dispatcher.on(event.test.started, test => {
110+
if (!options.video || state.startQueued) return
111+
state.startQueued = true
112+
recorder.add('screencast:start', async () => startScreencast(state.test, options, state), true)
113+
})
114+
109115
event.dispatcher.on(event.step.started, step => {
110116
if (state.steps) {
111117
const at = Date.now()
@@ -116,10 +122,6 @@ function wireScreencast(mode, options) {
116122
title: stepTitle(step),
117123
}
118124
}
119-
if (!options.video || state.startQueued || !state.test) return
120-
state.startQueued = true
121-
const test = state.test
122-
recorder.add('screencast:start', async () => startScreencast(test, options, state), true)
123125
})
124126

125127
if (options.subtitles) {

0 commit comments

Comments
 (0)