Skip to content

Stop reading a passed step's result as its error - #211

Merged
DavertMik merged 2 commits into
mainfrom
fix/step-logger-passed-result
Sep 15, 2026
Merged

DavertMik merged 2 commits into
mainfrom
fix/step-logger-passed-result

Conversation

@DavertMik

Copy link
Copy Markdown
Contributor

Found while investigating a failed Tester session (Langfuse trace 078285ca5b32d556be2695c50141c5c8, session VariableIncreasedBrown198). Every successful form call in that run reported a contradiction:

{
  "success": true,
  "message": "Form completed successfully with 4 commands.",
  "commandsExecuted": 4,
  "attempts": [{"command": "I.pressKey([\"Meta\",\"a\"])", "success": false, "error": "[object Promise]"}]
}

The bug

attachStepLogger binds one listener to both step.passed and step.failed, and reads argument 2 as the error either way:

const executed: ExecutedStep = { command: step.toCode(), success: !error };
if (error) {
  executed.error = errorToString(error);
  batchFailed = true;
}

But CodeceptJS emits the passing step's own return value as that argument — codeceptjs/lib/step/record.js:57:

event.emit(event.step.passed, step, val)   // val = step.run(...args)

val is a promise, so errorToString falls through to error.toString() and yields "[object Promise]".

The phantom entry is the visible half. The damage is the other half: it sets batchFailed = true, and if (batchFailed) return then drops every later step. That is why only one attempt appears where four commands ran, and why assertionsTarget keeps only the first assertion of a multi-line block — the same array that feeds recordVerification and the generated test file.

Pass/fail is unaffected: attempt() keys off whether execute() throws. What changes is what gets reported — which is what the AI reads before choosing its next action.

The fix

Pass only the step for step.passed.

Tests

The existing tests in tests/unit/executed-steps.test.ts all emit step.passed with a single argument, which is exactly why this stayed invisible. The new test emits the result alongside the step, as CodeceptJS does, and asserts all three passing steps are recorded. On main it records 1 of 3.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JCYmFZvUsENZBx71Yqojs6

DavertMik and others added 2 commits September 14, 2026 22:44
attachStepLogger bound one listener to both step.passed and step.failed
and treated the second argument as an error either way. CodeceptJS emits
`event.step.passed, step, val` (lib/step/record.js), where val is the
promise returned by step.run(), so every passing step arrived carrying a
truthy "error".

The first step of a batch was therefore recorded as failed with
`errorToString(promise)` — "[object Promise]" — and set batchFailed,
whose `if (batchFailed) return` gate then dropped every later step. A
four-command batch that fully succeeded reported one phantom failure and
nothing else, and assertionsTarget kept only the first assertion, which
is what feeds recordVerification and the generated test.

Pass only the step for step.passed. Pass/fail is unaffected — attempt()
keys off whether execute() throws — so this only corrects what gets
reported.

The existing tests emitted step.passed with a single argument, which is
why this stayed invisible; the new one emits the result alongside it, as
CodeceptJS does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JCYmFZvUsENZBx71Yqojs6
@DavertMik
DavertMik merged commit 8ee8a3c into main Sep 15, 2026
2 checks passed
@DavertMik
DavertMik deleted the fix/step-logger-passed-result branch September 15, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant