Skip to content

[WIP] Add logging for non-bug-related test failures in automated tests. - #6084

Draft
michelinewu wants to merge 4 commits into
masterfrom
mw_test_account_logs
Draft

michelinewu wants to merge 4 commits into
masterfrom
mw_test_account_logs

Conversation

@michelinewu

@michelinewu michelinewu commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Specify details for automated test failure cases

Automated test failure logs were too generic to rule out non-bug related failures.


Issues

**Every shutdown failure reported the same message.**Errors arrive as unknown causing webdriver to reject with Error objects when sometimes the api-client rejects with plain strings and with serialized error objects. The catch in stopApp emitted a blanket 'Crash on shutdown' regardless of what was thrown. That one message covers at least four unrelated conditions:

  1. The app never exited when waitForElectronInstancesExist timed out.
  2. The app exited early so the webdriver session was already gone.
  3. The window disappeared before closeWindow reached it.
  4. the connection to the app's api server dropped. Only this last one is a crash.

Account-caused failures looked identical to app bugs.
Failures due to issues from the user pool accounts, rather than app-related bugs, were logged the same in summary, requiring triage by hand in the logs for the specific test failure. This is unnecessarily time consuming. Specific cases:

  1. The test failed to obtain an account from the user pool after all retries completed.
  2. An account with YouTube is not enabled for live streaming.
  3. An account with YouTube cannot stream due to rate limiting.
  4. An account with YouTube failed for another account-related reason.

3. The analytics POST failing was silent.
sendJobToAnalytics wraps its request in a catch that logs and swallows it, so the entire body, including failed tests and stats, was discarded with no trace beyond one console line. This meant that test failures due to failure to post analytics was logged as ECONNABORTED and not distinguished from test failures due to app bugs.

Fixes

Shutdown errors are classified An error is passed as unknown and is now identified so it can be logged in a readable way. Note: the fallback case uses util.inspect rather than String() or JSON.stringify. String() renders any object without a .message as [object Object], so the message will not actually be logged. JSON.stringify is unsafe because it returns the value undefined for undefined/symbols, and it throws outright on circular references. util.inspect handles both and matches how main.js and app/app.ts already serialize unknown values into the log.

Account failures are detected and recorded. A new TAccountFailure union names the reasons a run failed because of its accounts:

Reason Detected from
NO_ACCOUNT_AVAILABLE flag set in reserveUserFromPool where it gives up after 5 attempts
YOUTUBE_STREAMING_DISABLED liveStreamingNotEnabled in the app log
YOUTUBE_ACCOUNT_RATE_LIMITED userRequestsExceedRateLimit in the app log
YOUTUBE_ACCOUNT_FAILURE token expired / platform validation failure in the app log
HEROKU_ANALYTICS_SEND_FAILED the analytics POST giving up

The user pool case NO_ACCOUNT_AVAILABLE is flagged at its source rather than string-matched because the harness never sees the AVA error. The YouTube cases come from the log because that is the only place the reason surfaces.

Detection reads currentTestLogs, which accumulates only the slice written since the current test began because the pre-existing lastLogs holds the whole file and leaks across tests whenever the cache directory is reused.

Results are written to test-dist/account-failures.json For extensibility, write results under a shape shared by both writers, described by the exported IAccountFailures:

{
  "tests": { "Go live to YouTube": "YOUTUBE_ACCOUNT_RATE_LIMITED" },
  "job": ["HEROKU_ANALYTICS_SEND_FAILED"]
}

tests is keyed by test name and written by the harness; job holds run-wide reasons and is written by the runner. Both sides read through a normalizer that fills in the missing halves, so an absent, empty, or older flat-shaped file degrades to empty instead of throwing. Job reasons are appended rather than assigned, so an earlier reason is not lost behind a later one.

Analytics carries the reason. Each entry in failedTests gains an accountFailure field, and the body gains a job-level accountFailure chosen off an explicit ACCOUNT_FAILURE_SEVERITY ordering rather than first-seen.

File(s) changed: test/helpers/webdriver/index.ts, test/helpers/webdriver/runner-utils.ts, test/helpers/webdriver/user.ts, test/helpers/runner.js

Performance Implications

None. All work happens in afterEach.always on the failure path only, after the test has already failed. Detection is a few regex matches over a string already held in memory, and the file write is a few hundred bytes to test-dist/. No change to the passing path.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves automated test triage by classifying common non-bug failure modes (shutdown issues, account/user-pool issues, and analytics-post failures) and persisting structured “account failure” metadata for both per-test and job-wide reporting.

Changes:

  • Adds more specific shutdown-failure logging by normalizing unknown thrown values into readable messages.
  • Detects account-related failure reasons from user-pool exhaustion and app logs, and records them to test-dist/account-failures.json.
  • Extends the CI runner analytics payload with per-test and job-level accountFailure fields, and records a job-wide reason if analytics submission fails.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
test/helpers/webdriver/user.ts Tracks user-pool exhaustion state for the current test to support downstream failure classification.
test/helpers/webdriver/runner-utils.ts Defines account-failure types and writes per-test account failure reasons to a shared JSON file.
test/helpers/webdriver/index.ts Captures per-test log slices for failure detection and improves shutdown error formatting/logging.
test/helpers/runner.js Aggregates and reports account-failure reasons in analytics; records job-wide reasons when analytics submission fails.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/helpers/webdriver/runner-utils.ts Outdated
Comment on lines +122 to +127
function readAccountFailures(): IAccountFailures {
const failures = fs.existsSync(ACCOUNT_FAILURES_PATH)
? JSON.parse(fs.readFileSync(ACCOUNT_FAILURES_PATH, 'utf8'))
: {};
return { tests: failures.tests ?? {}, job: failures.job ?? [] };
}
Comment thread test/helpers/webdriver/index.ts Outdated
* This is important because it indicates that the test failure is not a bug in the app.
* @returns - The type for the account reason the test failed
*/
function detectAccountFailure(): TAccountFailure {
@bundlemon

bundlemon Bot commented Aug 13, 2026

Copy link
Copy Markdown

BundleMon

Unchanged files (4)
Status Path Size Limits
renderer.(hash).js
10.34MB -
vendors~renderer.(hash).js
4.67MB -
updater.js
115.29KB -
guest-api.js
40.23KB -

No change in files bundle size

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

*/
export function saveAccountFailureToFile(testName: string, reason: TAccountFailure) {
const failures = readAccountFailures();
failures.tests[testName] = reason;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storing account failures by test name makes the reason survive into the retry run. If the first attempt fails with a YouTube/account issue and the retry then fails for a real app bug, sendJobToAnalytics() will still attach the stale accountFailure to that unrecovered failure. We should clear this test's account-failure entry before retrying/rerunning it, or record attempts separately, so final failures are not mislabeled as account noise.

Comment thread test/helpers/runner.js Outdated
await requestUtilityServer('job', 'post', body);
} catch (e) {
console.error('failed to send analytics', e);
saveJobAccountFailure('HEROKU_ANALYTICS_SEND_FAILED');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This records HEROKU_ANALYTICS_SEND_FAILED only after the analytics POST has already failed. The runner reads account-failures.json before this request, and the Azure job does not publish test-dist, so this reason never reaches analytics or any artifact; it only lands in a local file that disappears with the job. Either include this condition in something observable before/when the POST fails, or drop the file write and rely on the existing console error.

@michelinewu michelinewu changed the title Add logging for non-bug-related test failures in automated tests. [WIP] Add logging for non-bug-related test failures in automated tests. Aug 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

test/helpers/webdriver/runner-utils.ts:56

  • PR description says results are written to test-dist/account-failures.json, but the implementation writes to test-dist/failure-reasons.json. This mismatch makes it harder to locate artifacts and reason about the change. Please align the code and PR description (including any references in the runner/harness) to a single filename/shape.
const FAILURE_REASONS_PATH = 'test-dist/failure-reasons.json'; // failures because of known frequent non-bug reasons (like account issues) should be written in this file

test/helpers/runner.js:115

  • readFailureReason() can throw if failure-reasons.json parses to a non-object value (e.g. null), because failures.tests/failures.job are accessed outside the try/catch. This would crash the runner while sending analytics.
  } catch (e) {
    failures = {};
  }
  return { tests: failures.tests || {}, job: failures.job || [] };
}

Comment on lines +566 to +570
const failureReason = detectFailureReason();
if (detectAccountFailure(failureReason)) {
console.log(`Test failed because of failure reason: ${failureReason}`);
}
saveFailureReasonToFile(testName, failureReason);

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (4)

test/helpers/webdriver/index.ts:570

  • detectFailureReason() can return null, but its result is always passed into saveFailureReasonToFile. This triggers the "Expected failure reason but none provided" log for normal app-bug failures and prevents the failure-reasons file from being written, which in turn makes the runner log an error when it tries to read the file.
      const failureReason = detectFailureReason();
      if (detectAccountFailure(failureReason)) {
        console.log(`Test failed because of failure reason: ${failureReason}`);
      }
      saveFailureReasonToFile(testName, failureReason);

test/helpers/webdriver/index.ts:434

  • logsFromCurrentTest is intended to contain only logs written since the current test began, but logFileLastReadingPos is later set to logs.length - 1, so appending logs.slice(logFileLastReadingPos) re-includes the last character from the previous read. This can cause cross-test log contamination and (rarely) false-positive regex matches.
    const logs: string = await readLogs();
    if (!logs) return;
    lastLogs = logs;
    logsFromCurrentTest += logs.slice(logFileLastReadingPos);
    let ignoringErrors = false;

test/helpers/runner.js:115

  • readFailureReason() always attempts to readFileSync the failure-reasons file. When the file doesn't exist (e.g., no failures, or failures with no recognized reason), this throws ENOENT and logs an error that looks like a harness failure. Consider treating a missing file as the normal empty case and only logging when the file exists but can't be parsed.
function readFailureReason() {
  try {
    const failures = JSON.parse(fs.readFileSync(failureReasonsFile, 'utf8'));
    return { tests: failures?.tests || {}, job: failures?.job || [] };
  } catch (e) {

test/helpers/webdriver/runner-utils.ts:39

  • The PR description says results are written to test-dist/account-failures.json and includes a job-level reason like HEROKU_ANALYTICS_SEND_FAILED. The implementation writes test-dist/failure-reasons.json and the TFailureReason union doesn't include the described job-level reason (and the runner currently doesn't write any job reasons). Either update the PR description to match the implemented format, or adjust the code + runner to write/read the documented file name and reason set.
export type TFailureReason =
  | 'MISSING_TRANSLATIONS'
  | 'UNMOUNTED_REACT_UPDATE'
  | 'NO_ACCOUNT_AVAILABLE'
  | 'YOUTUBE_STREAMING_DISABLED'
  | 'YOUTUBE_ACCOUNT_RATE_LIMITED'
  | 'YOUTUBE_ACCOUNT_FAILURE'
  | 'UTILITY_SERVER_FAILURE';

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants