Contributing

Contributions are welcome. This page covers how to set up a development environment, project conventions, testing expectations, and the PR process.


Dev Setup

Requirements: Node.js >= 20, pnpm >= 10.

# 1. Fork https://github.com/Open-ACP/OpenACP and clone your fork
git clone https://github.com/<your-username>/OpenACP.git
cd OpenACP

# 2. Install dependencies
pnpm install

# 3. Compile TypeScript
pnpm build

# 4. Run the CLI locally
pnpm start

# 5. Watch mode (recompiles on save)
pnpm dev

# 6. Run the test suite
pnpm test

The compiled output lands in dist/. The entry point is dist/cli.js.

For publishing-related work, pnpm build:publish bundles via tsup into dist-publish/ — this is what gets shipped to npm as @openacp/cli.


Project Conventions

ESM-only

The package uses "type": "module". All import statements must use .js extensions, even when importing .ts source files:

TypeScript

  • strict: true is enforced.

  • Target: ES2022.

  • Module resolution: NodeNext.

  • All new files must be .ts. No plain .js in src/.

File and Module Layout

  • Source lives in src/. Tests live in src/**/__tests__/ or next to the file they test.

  • Core abstractions belong in src/core/. Adapter implementations belong in src/adapters/<name>/.

  • Public API exports flow through src/index.tssrc/core/index.ts.


Testing Guidelines

The test framework is Vitestarrow-up-right. Run with pnpm test (single run) or pnpm test:watch (interactive watch mode).

Test Flows, Not Internals

Tests should validate behavior against specifications, not implementation details. Focus on observable outcomes: what events are emitted, what methods are called, what state changes result.

Mock at Boundaries

Mock AgentInstance, ChannelAdapter, and SessionStore — not internal classes. Use vi.fn() for mocks. For event-driven mocks, use TypedEmitter from openacp:

Async Assertions

For fire-and-forget operations, use vi.waitFor() rather than arbitrary sleeps:

Use await Promise.resolve() for microtask timing when you just need to flush the microtask queue.

Timer-Based Tests

Use vi.useFakeTimers() for anything involving timeouts (e.g. permission gate expiry, session TTL):

What to Cover

Good contributions include tests for:

  • All state machine transitions — both valid paths and invalid ones. Verify events emitted on each.

  • Error recovery — after an error, the system must remain usable.

  • Concurrency — serial processing guarantees, queue ordering, race conditions.

  • Boundary values — at exactly the limit (e.g. maxConcurrentSessions).

  • Cleanup — timers, listeners, and files are removed in afterEach.

  • Idempotency — double connect(), double resolve(), double stop() must be safe.

Cleanup

Always clean up in afterEach:


PR Process

  1. Branch from develop, not main:

  2. Write tests before or alongside your implementation. PRs without tests for new behavior will be asked to add them.

  3. Build and test before pushing:

  4. Open a PR against develop. In your PR description:

    • Explain what the change does and why.

    • List any breaking changes.

    • Note any backward compatibility considerations (see the CLAUDE.md backward-compat section for the full policy).

  5. Backward compatibility: if you change config fields, storage formats, CLI commands, or plugin APIs, you must handle old data gracefully. New config fields need .default() or .optional() in the Zod schema. See the project CLAUDE.md for the full policy.


Code Style

  • Follow the patterns of the file you are editing — consistency within a file takes priority.

  • Prefer explicit types over any. Use unknown when the type is genuinely unknown.

  • Use createChildLogger({ module: 'my-module' }) for structured logging instead of console.log.

  • Keep files focused. If a file grows beyond ~300 lines, consider splitting it.

  • No default exports from core modules — use named exports so tree-shaking and refactoring tools work reliably.

Last updated

Was this helpful?