Skip to content

[ENG-2103] add E2E test helper class with CLI runner, retry utilities…#433

Open
leehpham wants to merge 5 commits intoproj/e2e-testsfrom
feat/ENG-2103
Open

[ENG-2103] add E2E test helper class with CLI runner, retry utilities…#433
leehpham wants to merge 5 commits intoproj/e2e-testsfrom
feat/ENG-2103

Conversation

@leehpham
Copy link
Copy Markdown
Member

…, and BrvE2EHelper.

Summary

  • Problem:
  • Why it matters:
  • What changed:
  • What did NOT change (scope boundary):

Type of change

  • Bug fix
  • New feature
  • Refactor (no behavior change)
  • Documentation
  • Test
  • Chore (build, dependencies, CI)

Scope (select all touched areas)

  • TUI / REPL
  • Agent / Tools
  • LLM Providers
  • Server / Daemon
  • Shared (constants, types, transport events)
  • CLI Commands (oclif)
  • Hub / Connectors
  • Cloud Sync
  • CI/CD / Infra

Linked issues

  • Closes #
  • Related #

Root cause (bug fixes only, otherwise write N/A)

  • Root cause:
  • Why this was not caught earlier:

Test plan

  • Coverage added:
    • Unit test
    • Integration test
    • Manual verification only
  • Test file(s):
  • Key scenario(s) covered:

User-visible changes

List user-visible changes (including defaults, config, or CLI output).
If none, write None.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording

Checklist

  • Tests added or updated and passing (npm test)
  • Lint passes (npm run lint)
  • Type check passes (npm run typecheck)
  • Build succeeds (npm run build)
  • Commits follow Conventional Commits format
  • Documentation updated (if applicable)
  • No breaking changes (or clearly documented above)
  • Branch is up to date with main

Risks and mitigations

List real risks for this PR. If none, write None.

  • Risk:
    • Mitigation:

@leehpham leehpham requested a review from bao-byterover as a code owner April 16, 2026 10:34
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

PR Review: [ENG-2103] E2E Test Helpers

  • Gather context and read changed files
  • Review retry.ts and retry.test.ts
  • Review cli-runner.ts and cli-runner.test.ts
  • Review brv-e2e-helper.ts and brv-e2e-helper.test.ts
  • Review helpers/index.ts
  • Post inline comments
  • Post overall summary with score

View job run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

Claude finished @leehpham's task in 2m 45s —— View job


PR Review: [ENG-2103] E2E Test Helpers

  • Gather context and read changed files
  • Review retry.ts and retry.test.ts
  • Review cli-runner.ts and cli-runner.test.ts
  • Review brv-e2e-helper.ts and brv-e2e-helper.test.ts
  • Review helpers/index.ts
  • Post inline comments
  • Post overall summary with score

Summary

Score: 4 / 5

Solid E2E test infrastructure. The design is clean — BrvE2eHelper encapsulates temp-dir lifecycle, LIFO teardowns, and JSON parsing in a way that will compose well in real E2E suites. retry and waitUntil are correct, well-tested, and the ESLint disable comments are appropriate.

Issues to fix before merge:

Severity Location Issue
issue cli-runner.ts:45 resolve Promise parameter shadows the node:path import — rename to fulfill
issue brv-e2e-helper.ts:114 setup() is not idempotent — calling it twice orphans the first temp directory
issue brv-e2e-helper.test.ts:49 Hardcoded '0.0.1' version string — use imported BRV_CONFIG_VERSION constant
nitpick cli-runner.ts:24 Hardcoded tsx/dist/esm/index.mjs internal path is fragile against tsx version bumps
nitpick retry.test.ts:70 Timing assertion with 20% margin can fail on slow CI — widen or restructure
nitpick brv-e2e-helper.test.ts:19 Describe block name 'BrvE2EHelper' doesn't match class BrvE2eHelper

What's good:

  • retry and waitUntil are correct and have excellent coverage (6 + 3 tests)
  • LIFO teardown ordering with swallowed errors is the right design for cleanup
  • runJson scanning from the last line backwards handles CLI preamble output gracefully
  • The before(requireE2eEnv) guard correctly skips auth tests when no E2E key is present
  • Barrel index.ts exports all public surface cleanly
  • process.env spread into child env with config override is the right layering approach

// so tsx resolves correctly regardless of the child process cwd
const nodeArgs = ['--import', TSX_IMPORT_PATH, '--no-warnings', BIN_DEV_PATH, ...args]

return new Promise((resolve) => {
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.

issue (naming/shadowing): The Promise callback parameter resolve shadows the resolve import from node:path. This will likely trigger the no-shadow ESLint rule and is genuinely confusing — a reader seeing resolve({exitCode, ...}) on line 52 has to mentally track which resolve is in scope.

Suggested change
return new Promise((resolve) => {
return new Promise((fulfill) => {

Then update lines 52 and 58 to use fulfill(...) instead. Fix this →

const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
const BIN_DEV_PATH = resolve(PROJECT_ROOT, 'bin', 'dev.js')
// Resolve tsx from project root so it works even when cwd is a temp dir
const TSX_IMPORT_PATH = resolve(PROJECT_ROOT, 'node_modules', 'tsx', 'dist', 'esm', 'index.mjs')
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.

nitpick (fragile path): Hardcoding tsx/dist/esm/index.mjs couples the runner to tsx's internal directory layout. If tsx restructures its package (as it has done between versions), this silently breaks E2E runs.

Consider resolving via the bin field instead:

// More resilient: find tsx via its package.json#bin map
const TSX_BIN_PATH = resolve(PROJECT_ROOT, 'node_modules', '.bin', 'tsx')

Or use require.resolve/import.meta.resolve with a try-catch fallback. Either approach survives tsx version bumps.

throw new Error(`No valid JSON found in CLI output.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`)
}

async setup(): Promise<void> {
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.

issue (resource leak): setup() is not idempotent. Calling it twice creates a new temp dir and overwrites _cwd, orphaning the first directory without cleaning it up.

Add a guard (or call cleanup() first) to prevent the leak:

Suggested change
async setup(): Promise<void> {
async setup(): Promise<void> {
if (this._cwd) {
await this.cleanup()
}
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'brv-e2e-')))

Alternatively, simply throw if already set up — forcing explicit cleanup before re-use. Fix this →

expect(existsSync(configPath)).to.be.true

const config = JSON.parse(readFileSync(configPath, 'utf8'))
expect(config).to.deep.equal({version: '0.0.1'})
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.

issue (magic string): Hardcoding '0.0.1' couples this test to the constant's current value. If BRV_CONFIG_VERSION is bumped, this test breaks with a confusing mismatch. Use the imported constant directly:

Suggested change
expect(config).to.deep.equal({version: '0.0.1'})
expect(config).to.deep.equal({version: BRV_CONFIG_VERSION})

BRV_CONFIG_VERSION is already imported at line 8 of brv-e2e-helper.ts — add it to this test's imports too. Fix this →

const elapsed = Date.now() - start

// 2 retries * 50ms delay = at least 100ms
expect(elapsed).to.be.at.least(80) // small margin for timer imprecision
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.

nitpick (flaky timing test): The 80ms lower bound on a 100ms expected delay is only a 20% margin. On a heavily loaded CI runner this can fail spuriously. A slightly tighter lower bound assertion (e.g., at.least(60)) or switching to counting calls instead of timing would make this more reliable.

Alternatively, verify that the delay utility is called rather than relying on wall-clock elapsed time — but since sleep is a module-local unexported function that pattern isn't practical here. Widening the margin to at.least(50) is the safest path.

webAppUrl: 'http://localhost:0',
}

describe('BrvE2EHelper', () => {
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.

nitpick (naming inconsistency): The describe block uses 'BrvE2EHelper' but the class is BrvE2eHelper. Minor, but matching the actual class name reduces confusion when searching logs or test output.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 17, 2026

Claude finished @leehpham's task in 1m 52s —— View job


PR Review: [ENG-2103] E2E Test Helpers

  • Gather context and read changed files
  • Review retry.ts and retry.test.ts
  • Review cli-runner.ts and cli-runner.test.ts
  • Review brv-e2e-helper.ts and brv-e2e-helper.test.ts
  • Review helpers/index.ts
  • Check if previous review issues were addressed
  • Post overall summary with score

Summary

Score: 3.5 / 5

The PR was synchronized (merge commits from proj/e2e-tests), but the implementation code is unchanged since the initial commit. None of the issues flagged in the previous review have been addressed. Re-surfacing them here — these must be fixed before merge.


Outstanding Issues (from previous review, still present)

Severity Location Issue
issue cli-runner.ts:45 resolve Promise callback parameter shadows the resolve import from node:path
issue brv-e2e-helper.ts:114 setup() is not idempotent — calling it twice orphans the first temp directory
issue brv-e2e-helper.test.ts:49 Hardcoded '0.0.1' — use BRV_CONFIG_VERSION constant instead
nitpick cli-runner.ts:24 Hardcoded tsx/dist/esm/index.mjs internal path is fragile against tsx version bumps
nitpick retry.test.ts:70 80ms lower bound for a 100ms target is only 20% margin — can flake on slow CI
nitpick brv-e2e-helper.test.ts:19 describe('BrvE2EHelper', ...) doesn't match class name BrvE2eHelper

New Issues Found

cli-runner.ts:29–30 — Unsafe process.env cast

...process.env as Record<string, string>,

process.env values are string | undefined, but the cast tells TypeScript they are always string. Any unset var gets spread as undefined into childEnv, which could cause subtle bugs (e.g., a child process receives "undefined" as a string value). Filter out undefined entries before spreading:

...Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined)) as Record<string, string>,

Fix this →

brv-e2e-helper.ts:67–73 — Double-logout risk in login()

login() auto-registers logout as a teardown. If a test also calls helper.logout() manually, cleanup will try to logout a second time. Since teardown errors are swallowed this is safe but noisy. Consider tracking login state (private _loggedIn = false) to guard the teardown call.

env-guard.ts:30 — Formatting (double space)

export const getE2eConfig =  (): E2eConfig => {

Stray double space before (): — minor but worth a fix to keep lint clean.


What's Good

  • LIFO teardown ordering with swallowed errors is the right design
  • runJson scanning backwards from the last line handles CLI preamble output correctly
  • before(requireE2eEnv) guard correctly skips auth tests when no E2E key is configured
  • retry and waitUntil are correct and well-tested
  • Barrel index.ts exports all public surface cleanly
  • process.env spread into child env with config override is the right layering

Action required: The three issue-level items (shadowed resolve, non-idempotent setup(), hardcoded version string) must be resolved before this can be merged.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 17, 2026

Claude finished @leehpham's task in 1m 25s —— View job


PR Review: [ENG-2103] E2E Test Helpers

  • Gather context
  • Read changed files
  • Check if previous review issues were addressed
  • Post overall summary with score

Summary

Score: 3 / 5

This is the third review cycle. The PR was synchronized (merge commits from main), but none of the implementation files have changed since the initial commit. All issues flagged in both previous reviews remain unaddressed. The score drops to 3/5.


Outstanding Issues — All Still Present

Severity Location Issue
issue cli-runner.ts:45 resolve Promise callback parameter shadows the resolve import from node:path
issue brv-e2e-helper.ts:114 setup() is not idempotent — calling it twice orphans the first temp directory
issue brv-e2e-helper.test.ts:49 Hardcoded '0.0.1' — use BRV_CONFIG_VERSION constant instead
issue cli-runner.ts:29–30 ...process.env as Record<string, string> spreads undefined values into child env
nitpick cli-runner.ts:24 Hardcoded tsx/dist/esm/index.mjs internal path is fragile against tsx version bumps
nitpick retry.test.ts:70 80ms lower bound on a 100ms target is only 20% margin — can flake on slow CI
nitpick brv-e2e-helper.test.ts:19 describe('BrvE2EHelper', ...) doesn't match class name BrvE2eHelper
nitpick env-guard.ts:30 Double space: export const getE2eConfig = (): E2eConfig => {

Required Fixes Before Merge

1. cli-runner.ts:45 — Rename resolve Promise parameter

// before
return new Promise((resolve) => {
// after
return new Promise((fulfill) => {

Update lines 50, 52, and 58 to call fulfill(...) instead. Fix this →

2. brv-e2e-helper.ts:114 — Guard setup() against double-call

async setup(): Promise<void> {
  if (this._cwd) {
    await this.cleanup()
  }
  const dir = realpathSync(mkdtempSync(join(tmpdir(), 'brv-e2e-')))
  // ...
}

Fix this →

3. brv-e2e-helper.test.ts:49 — Replace hardcoded version string

// before
expect(config).to.deep.equal({version: '0.0.1'})
// after
expect(config).to.deep.equal({version: BRV_CONFIG_VERSION})

Import BRV_CONFIG_VERSION from '../../../src/server/constants.js'. Fix this →

4. cli-runner.ts:29–30 — Filter undefined env vars before spreading

// before
...process.env as Record<string, string>,
// after
...Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined)) as Record<string, string>,

Fix this →


What's Good

  • LIFO teardown ordering with swallowed errors is the right design
  • runJson scanning from the last line backwards handles CLI preamble output correctly
  • before(requireE2eEnv) guard correctly skips auth tests when no E2E key is configured
  • retry and waitUntil are correct and well-tested (6 + 3 tests)
  • Barrel index.ts exports all public surface cleanly

Action required: The four issue-level items must be resolved before this PR can be merged. This is the third review cycle — please address these before the next sync.

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