-
Notifications
You must be signed in to change notification settings - Fork 446
[ENG-2103] add E2E test helper class with CLI runner, retry utilities… #433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: proj/e2e-tests
Are you sure you want to change the base?
Changes from all commits
1089e69
b382a69
ac2436e
018325e
f711671
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| # Repository Guidelines | ||
|
|
||
| ByteRover CLI (`brv`) - Interactive REPL with React/Ink TUI | ||
|
|
||
| ## Commands | ||
|
|
||
| ```bash | ||
| npm run build # Compile to dist/ | ||
| npm run dev # Kill daemon + build + run dev mode | ||
| npm test # All tests | ||
| npx mocha --forbid-only "test/path/to/file.test.ts" # Single test | ||
| npm run lint # ESLint | ||
| npm run typecheck # TypeScript type checking | ||
| ./bin/dev.js [command] # Dev mode (ts-node) | ||
| ./bin/run.js [command] # Prod mode | ||
| ``` | ||
|
|
||
| **Test dirs**: `test/commands/`, `test/unit/`, `test/integration/`, `test/hooks/`, `test/learning/`, `test/helpers/`, `test/shared/` | ||
| **Note**: Run tests from project root, not within test directories | ||
|
|
||
| ## Development Standards | ||
|
|
||
| **TypeScript**: | ||
| - Avoid `as Type` assertions - use type guards or proper typing instead | ||
| - Avoid `any` type - use `unknown` with type narrowing or proper generics | ||
| - Functions with >3 parameters must use object parameters | ||
| - Prefer `type` for data-only shapes (DTOs, payloads, configs); prefer `interface` for behavioral contracts with method signatures (services, repositories, strategies) | ||
| - Default to `undefined` over `null`; reserve `null` only for external boundaries (storage, HTTP APIs) that force it — normalize to `undefined` before the value flows into internal modules | ||
| - Avoid `!` non-null assertions — narrow with type guards or throw explicitly. Lazy-initialized singletons (e.g. `this.services!.foo` after a guaranteed init step) are the only acceptable exception | ||
| - Use `??` for nullish defaults (not `||`, which also triggers on `0`/`''`/`false`) and `?.` for safe property access | ||
| - Prefer optional properties (`foo?: T`) over `foo: T | undefined` when a key may legitimately be absent | ||
|
|
||
| **Testing (Strict TDD — MANDATORY)**: | ||
| - You MUST follow Test-Driven Development. This is non-negotiable. | ||
| - **Step 1 — Write failing tests FIRST**: Before writing ANY implementation code, write or update tests that describe the expected behavior. Do NOT write implementation and tests together or in reverse order. | ||
| - **Step 2 — Run tests to confirm they fail**: Execute the relevant test file to verify the new tests fail for the right reason (missing implementation, not a syntax error). | ||
| - **Step 3 — Write the minimal implementation**: Write only enough code to make the failing tests pass. Do not add untested behavior. | ||
| - **Step 4 — Run tests to confirm they pass**: Execute tests again to verify all tests pass. | ||
| - **Step 5 — Refactor if needed**: Clean up while keeping tests green. | ||
| - If you catch yourself writing implementation code without a failing test, STOP and write the test first. | ||
| - 80% coverage minimum, critical paths must be covered. | ||
| - Suppress console logging in tests to keep output clean. | ||
| - Unit tests must run fast and run completely in memory. Proper stubbing and mocking must be implemented. | ||
|
|
||
| **Feature Development (Outside-In Approach — applies to ALL work: planning, reviewing, coding, and auditing)**: | ||
| - This is a foundational principle, not just a coding guideline. Apply it when writing code, reviewing plans, designing milestones, evaluating project structure, or auditing existing work. If a plan, project, or milestone ordering violates Outside-In, flag it. | ||
| - Start from the consumer (oclif command, REPL command, or TUI component) — understand what it needs | ||
| - Define the minimal interface — only what the consumer actually requires | ||
| - Implement the service — fulfill the interface contract | ||
| - Extract entities only if needed — when shared structure emerges across multiple consumers | ||
| - Avoid designing in isolation — always have a concrete consumer driving requirements | ||
| - When reviewing or planning: if entities, types, or store interfaces are designed before any consumer exists to validate them, that is Inside-Out and must be flagged | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Source Layout (`src/`) | ||
|
|
||
| - `agent/` — LLM agent: `core/` (interfaces/domain), `infra/` (23 modules, including llm, memory, map, swarm, tools, document-parser), `resources/` (prompts YAML, tool `.txt` descriptions) | ||
| - `server/` — Daemon infrastructure: `config/`, `core/` (domain/interfaces), `infra/` (30 modules, including vc, git, hub, mcp, cogit, project, provider-oauth, space, dream), `templates/`, `utils/` | ||
| - `shared/` — Cross-module: constants, types, transport events, utils | ||
| - `tui/` — React/Ink TUI: app (router/pages), components, features (23 modules, including vc, worktree, source, hub, curate), hooks, lib, providers, stores | ||
| - `oclif/` — Commands grouped by topic (`vc/`, `hub/`, `worktree/`, `source/`, `space/`, `review/`, `connectors/`, `curate/`, `model/`, `providers/`, `swarm/`, `query-log/`) + top-level `.ts` commands; hooks, lib (daemon-client, task-client, json-response) | ||
|
|
||
| **Import boundary** (ESLint-enforced): `tui/` must not import from `server/`, `agent/`, or `oclif/`. Use transport events or `shared/`. | ||
|
|
||
| ### REPL + TUI | ||
|
|
||
| - `brv` (no args) starts REPL (`src/tui/repl-startup.tsx`) | ||
| - Esc cancels streaming responses and long-running commands | ||
| - Slash commands in `src/tui/features/commands/definitions/` (order in `index.ts` = suggestion order) | ||
|
|
||
| ### Daemon | ||
|
|
||
| - Global daemon (`server/infra/daemon/`) hosts Socket.IO transport; clients connect via `@campfirein/brv-transport-client` | ||
| - Agent pool manages forked child processes per project; task routing in `server/infra/process/` | ||
| - MCP server in `server/infra/mcp/` exposes tools via Model Context Protocol; `tools/` subdir has dedicated implementations (`brv-query-tool`, `brv-curate-tool`) | ||
|
|
||
| ### VC, Worktrees & Knowledge Sources | ||
|
|
||
| - `brv vc` — isomorphic-git version control (add, branch, checkout, clone, commit, config, fetch, init, log, merge, pull, push, remote, reset, status); git plumbing in `server/infra/git/` (`isomorphic-git-service.ts`), VC config store in `server/infra/vc/` | ||
| - `brv worktree` (add/list/remove) — git-style worktree pointer model: `.brv/` is either a real project directory OR a pointer file to a parent project; parent stores registry in `.brv/worktrees/<name>/link.json` | ||
| - `brv source` (add/list/remove) — link another project's context tree as a read-only knowledge source with write isolation | ||
| - `brv search <query>` — pure BM25 retrieval over the context tree (minisearch, no LLM, no token cost); structured results with paths/scores. Pairs with `brv query` (LLM-synthesized answer). Engine: `server/infra/executor/search-executor.ts` | ||
| - `brv locations` — lists all registered projects with context-tree status (text or `--format json`); reads from `LocationsEvents` over the daemon transport | ||
| - `brv query-log view [id]` / `brv query-log summary` — inspect query history and recall metrics (coverage, cache hit rate, top topics); store: `server/infra/storage/file-query-log-store.ts`, summary use-case in `server/infra/usecase/` | ||
| - `brv dream [--force] [--undo] [--detach]` — background context-tree consolidation (synthesize/consolidate/prune); engine: `server/infra/dream/` | ||
| - Canonical project resolver: `resolveProject()` in `server/infra/project/` — priority `flag > direct > linked > walked-up > null`. `projectRoot` and `worktreeRoot` are threaded through transport schemas, task routing, and all executors | ||
| - All commands are daemon-routed: `oclif/` and `tui/` never import from `server/` | ||
| - Oclif: `src/oclif/commands/{vc,worktree,source}/`; TUI: `src/tui/features/{vc,worktree,source}/`; slash commands (`vc-*`, `worktree`, `source`) in `src/tui/features/commands/definitions/` | ||
|
|
||
| ### Agent (`src/agent/`) | ||
|
|
||
| - Tools: definitions in `resources/tools/*.txt`, implementations in `infra/tools/implementations/`, registry in `infra/tools/tool-registry.ts` | ||
| - Tool categories: file ops (read/write/edit/glob/grep/list-dir), bash (exec/output), knowledge (create/expand/search), memory (read/write/edit/delete/list), swarm (query/store), todos (read/write), curate, code exec, batch, detect domains, kill process, search history | ||
| - LLM: 18 providers in `infra/llm/providers/`; 6 compression strategies in `infra/llm/context/compression/` | ||
| - System prompts: contributor pattern (XML sections) in `infra/system-prompt/` | ||
| - Map/memory: `infra/map/` (agentic map, context-tree store, LLM map memory, worker pool); `infra/memory/` (memory-manager, deduplicator) | ||
| - Storage: file-based blob (`infra/blob/`) and key storage (`infra/storage/`) — no SQLite | ||
|
|
||
| ### Swarm (`src/agent/infra/swarm/`, `src/oclif/commands/swarm/`) | ||
|
|
||
| - Multi-provider memory/knowledge federation: routes queries and writes across pluggable adapters (byterover, gbrain, local-markdown, memory-wiki, obsidian) | ||
| - `brv swarm query` — RRF-fused search across providers; flags: `--explain`, `--format`, `-n` | ||
| - `brv swarm curate` — auto-routes content to best provider; flags: `--provider`, `--format` | ||
| - `brv swarm onboard` — interactive wizard (`@inquirer/prompts`) to scaffold swarm config; uses snake_case YAML keys (`eslint-disable camelcase`) | ||
| - `brv swarm status` — pre-flight health check for configured providers | ||
| - Agent tools: `swarm_query.txt`, `swarm_store.txt` in `resources/tools/` | ||
| - Config: `swarm/config/` (loader + schema), `swarm/validation/` (config validator) | ||
| - CLI-only (oclif) — no TUI feature dir; swarm queries flow through existing `tui/features/query/` | ||
|
|
||
| ## Testing Gotchas | ||
|
|
||
| - **HTTP (nock)**: Must verify `.matchHeader('authorization', ...)` + `.matchHeader('x-byterover-session-id', ...)` | ||
| - **ES Modules**: Cannot stub ES exports with sinon; test utils with real filesystem (`tmpdir()`) | ||
|
|
||
| ## Conventions | ||
|
|
||
| - ES modules with `.js` import extensions required | ||
| - `I` prefix for interfaces; `toJson()`/`fromJson()` (capital J) for serialization | ||
| - Snake_case APIs: `/* eslint-disable camelcase */` | ||
|
|
||
| ## Environment | ||
|
|
||
| - `BRV_ENV` — `development` | `production` (dev-only commands require `development`, set by `bin/dev.js` and `bin/run.js`) | ||
|
|
||
| ## Stack | ||
|
|
||
| oclif v4, TypeScript (ES2022, Node16 modules, strict), React/Ink (TUI), Zustand, axios, socket.io, isomorphic-git, Mocha + Chai + Sinon + Nock |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,158 @@ | ||||||
| import {expect} from 'chai' | ||||||
| import {existsSync, readFileSync} from 'node:fs' | ||||||
| import {join} from 'node:path' | ||||||
|
|
||||||
| import type {E2eConfig} from './env-guard.js' | ||||||
|
|
||||||
| import {BrvE2eHelper} from './brv-e2e-helper.js' | ||||||
| import {getE2eConfig, requireE2eEnv} from './env-guard.js' | ||||||
|
|
||||||
| const dummyConfig: E2eConfig = { | ||||||
| apiBaseUrl: 'http://localhost:0', | ||||||
| apiKey: 'test-key', | ||||||
| cogitApiBaseUrl: 'http://localhost:0', | ||||||
| gitRemoteBaseUrl: 'http://localhost:0', | ||||||
| llmApiBaseUrl: 'http://localhost:0', | ||||||
| webAppUrl: 'http://localhost:0', | ||||||
| } | ||||||
|
|
||||||
| describe('BrvE2EHelper', () => { | ||||||
| describe('mechanics', () => { | ||||||
| let helper: BrvE2eHelper | ||||||
|
|
||||||
| beforeEach(() => { | ||||||
| helper = new BrvE2eHelper(dummyConfig) | ||||||
| }) | ||||||
|
|
||||||
| afterEach(async () => { | ||||||
| await helper.cleanup() | ||||||
| }) | ||||||
|
|
||||||
| it('should instantiate with E2eConfig', () => { | ||||||
| expect(helper).to.be.instanceOf(BrvE2eHelper) | ||||||
| }) | ||||||
|
|
||||||
| it('should throw when accessing cwd before setup()', () => { | ||||||
| expect(() => helper.cwd).to.throw('setup() must be called') | ||||||
| }) | ||||||
|
|
||||||
| it('should create a temp directory with .brv/config.json on setup()', async () => { | ||||||
| await helper.setup() | ||||||
|
|
||||||
| expect(helper.cwd).to.be.a('string').that.is.not.empty | ||||||
| expect(existsSync(helper.cwd)).to.be.true | ||||||
|
|
||||||
| const configPath = join(helper.cwd, '.brv', 'config.json') | ||||||
| expect(existsSync(configPath)).to.be.true | ||||||
|
|
||||||
| const config = JSON.parse(readFileSync(configPath, 'utf8')) | ||||||
| expect(config).to.deep.equal({version: '0.0.1'}) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (magic string): Hardcoding
Suggested change
|
||||||
| }) | ||||||
|
|
||||||
| it('should remove the temp directory on cleanup()', async () => { | ||||||
| await helper.setup() | ||||||
| const dir = helper.cwd | ||||||
|
|
||||||
| await helper.cleanup() | ||||||
|
|
||||||
| expect(existsSync(dir)).to.be.false | ||||||
| expect(() => helper.cwd).to.throw('setup() must be called') | ||||||
| }) | ||||||
|
|
||||||
| it('should run all registered teardown functions during cleanup() in reverse order', async () => { | ||||||
| await helper.setup() | ||||||
|
|
||||||
| const order: number[] = [] | ||||||
| helper.onTeardown(async () => { order.push(1) }) | ||||||
| helper.onTeardown(async () => { order.push(2) }) | ||||||
| helper.onTeardown(async () => { order.push(3) }) | ||||||
|
|
||||||
| await helper.cleanup() | ||||||
|
|
||||||
| expect(order).to.deep.equal([3, 2, 1]) | ||||||
| }) | ||||||
|
|
||||||
| it('should be safe to call cleanup() multiple times', async () => { | ||||||
| await helper.setup() | ||||||
| await helper.cleanup() | ||||||
| await helper.cleanup() // should not throw | ||||||
| }) | ||||||
|
|
||||||
| it('should still cleanup temp dir if a teardown throws', async () => { | ||||||
| await helper.setup() | ||||||
| const dir = helper.cwd | ||||||
|
|
||||||
| const ran: number[] = [] | ||||||
| helper.onTeardown(async () => { ran.push(1) }) | ||||||
| helper.onTeardown(async () => { throw new Error('teardown failed') }) | ||||||
| helper.onTeardown(async () => { ran.push(3) }) | ||||||
|
|
||||||
| // cleanup should not throw despite the failing teardown | ||||||
| await helper.cleanup() | ||||||
|
|
||||||
| expect(existsSync(dir)).to.be.false | ||||||
| expect(ran).to.deep.equal([3, 1]) // reverse order, skipping the one that threw | ||||||
| }) | ||||||
|
|
||||||
| it('should run a CLI command and return the result', async () => { | ||||||
| await helper.setup() | ||||||
|
|
||||||
| const result = await helper.run('--help') | ||||||
|
|
||||||
| expect(result.exitCode).to.equal(0) | ||||||
| expect(result.stdout).to.include('USAGE') | ||||||
| expect(result.stderr).to.be.a('string') | ||||||
| }) | ||||||
|
|
||||||
| it('should throw when runJson() receives non-JSON output', async () => { | ||||||
| await helper.setup() | ||||||
|
|
||||||
| try { | ||||||
| await helper.runJson('--help') | ||||||
| expect.fail('should have thrown') | ||||||
| } catch (error) { | ||||||
| expect(error).to.be.instanceOf(Error) | ||||||
| expect((error as Error).message).to.include('No valid JSON') | ||||||
| } | ||||||
| }) | ||||||
| }) | ||||||
|
|
||||||
| describe('auth (requires E2E env)', () => { | ||||||
| before(requireE2eEnv) | ||||||
|
|
||||||
| let helper: BrvE2eHelper | ||||||
|
|
||||||
| beforeEach(async () => { | ||||||
| const config = getE2eConfig() | ||||||
| helper = new BrvE2eHelper(config) | ||||||
| await helper.setup() | ||||||
| }) | ||||||
|
|
||||||
| afterEach(async () => { | ||||||
| await helper.cleanup() | ||||||
| }) | ||||||
|
|
||||||
| it('should login with the configured API key', async () => { | ||||||
| const result = await helper.login() | ||||||
|
|
||||||
| // login() returns void on success, throws on failure | ||||||
| expect(result).to.be.undefined | ||||||
| }) | ||||||
|
|
||||||
| it('should logout after login', async () => { | ||||||
| await helper.login() | ||||||
| const result = await helper.logout() | ||||||
|
|
||||||
| expect(result).to.be.undefined | ||||||
| }) | ||||||
|
|
||||||
| it('should parse JSON response via runJson()', async () => { | ||||||
| const result = await helper.runJson<{userEmail?: string}>('login', ['--api-key', getE2eConfig().apiKey]) | ||||||
|
|
||||||
| expect(result).to.have.property('command', 'login') | ||||||
| expect(result).to.have.property('success').that.is.a('boolean') | ||||||
| expect(result).to.have.property('data').that.is.an('object') | ||||||
| expect(result).to.have.property('timestamp').that.is.a('string') | ||||||
| }) | ||||||
| }) | ||||||
| }) | ||||||
There was a problem hiding this comment.
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 isBrvE2eHelper. Minor, but matching the actual class name reduces confusion when searching logs or test output.