This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
These values act as a filter for every proposed change. Reject anything that violates them.
- Zero dependencies — no runtime npm dependencies in library code (see
doc/decisions/0003-zero-dependencies.md). Refuse any change that adds animportfrom an external package insidelib/orbin/. - No metaprogramming — no Proxies, no
Object.defineProperty, no__proto__assignment. Every property access must follow the normal prototype chain. Every line must be readable in a classroom without prior explanation. - OOP in the Smalltalk spirit — behaviour through message sends and polymorphism, not conditionals over types. Add a method to an object before adding a
switch/ifchain in a caller.
npm test # run all tests (self-tests the framework)
npm test tests/path/file_test.js # run a single test file
npm run lint # ESLint check
npm run lint:fix # ESLint auto-fix
npm run playground:reset # copy template → tests/playground_test.js
npm run playground:run # run playground file
npm run playground:clear # delete playground file
npm run test:coverage # coverage report via c8 (see reports/coverage/)
npm run test:mutation # mutation testing via Stryker (slow, see reports/mutation/)Node 22+ is required. The repo uses asdf; .tool-versions pins nodejs 22.21.0.
bin/testy_cli.js → ScriptAction.for(params) (handles -h, -v, or RunTests) → Testy#run() → loads test files via dynamic import() → TestRunner#run() → emits callbacks → ConsoleUI → Formatter.
TestRunner— orchestrates suites; aggregates counts; drives theonFinish/onSuccess/onFailurecallbacks.TestSuite— owns a list ofTestinstances; runs each; firesonStart/onFinishsuite callbacks.Test— executes one test body; stores aTestResult; fireswhenSuccess/whenFailed/whenErrored/whenPending/whenSkippedcallbacks.Asserter/Assertion— assertion DSL. Primary form:assert.that(actual).isEqualTo(expected). Shorthand form:assert.areEqual(actual, expected). Custom failure message:assert.withDescription('msg').isTrue(expr).
ConsoleUI is the event hub. It converts TestRunner/TestSuite/Test callbacks into calls on the active Formatter.
Formatter (base class) defines the full event protocol as no-op methods. Concrete subclasses:
ConsoleFormatter— human-readable, coloured output (default)TapFormatter— TAP version 13 (streaming, one line per test)JsonFormatter— single JSON document emitted ondisplayRunnerEnd
FormatterFactory.for(output, console, i18n) maps the output config key to the right class; unknown values fall back to ConsoleFormatter.
Configuration merges .testyrc.json (user, optional) over default_configuration.json. Keys: directory, filter, language, failFast, randomOrder, timeoutMs, output.
ParametersParser maps CLI flags to a config object. Paired flags (-l, -d, -e, -o) are sanitized into "flag value" strings before dispatch to per-flag parser classes.
translations.json holds all UI strings in en, es, it, pt. Failure messages implement .expressedIn(i18n) so they resolve lazily in the configured language. Adding a translation key requires entries in all four language sections — tests/core/translation_keys_consistency_test.js will catch any missing ones.
tests_factory.js— factories forTestinstances in each result state (aPassingTest,aFailingTest,anErroredTest,aPendingTest,anExplicitlySkippedTest). Factories accept anasserterargument.formatter_helpers.js—runResultsWith(suiteName, ...factories)runs tests and returns{ runner, suite };driveFormatter(formatter, runner, suite)replays the full event stream into a formatter.runner_helpers.js/suites_factory.js— lower-level helpers for building runners and suites in tests.
A CI check that blocks PRs violating Testy's simplicity contract. It runs on every PR against main.
See doc/decisions/0017-simplicity-guardian.md for the full decision record.
Run locally:
node bin/simplicity-guardian.js # text output, exits 1 on violations
node bin/simplicity-guardian.js --format json # machine-readable outputThree layers checked:
- Zero-dependency — no external package imports in
lib/orbin/ - Metaprogramming — no
new Proxy,Object.defineProperty, or__proto__= - Fan-out — no file with more than 7 imports
ESLint note: class-methods-use-this is intentionally absent from Testy's ESLint config.
In Smalltalk-style OOP, methods are polymorphic message handlers — a method that doesn't reference
this today may still be meant for subclass override. OO purity takes precedence over JS idioms.
- Pure ES modules —
"type": "module"throughout. No CommonJSrequireinlib/, exceptcreateRequirefor reading JSON files (e.g.package.json). - Private fields — use
#fieldNamesyntax (not underscore prefixes). - No
for...of/for...ininlib/— ESLint enforces this; use array iteration methods instead. Thefor...ofban is explicitlyeslint-disabled in the two places where sequential async iteration is unavoidable (testy.js). no-empty-function— no-op methods in base classes must have a// no-opcomment inside the body.prefer-destructuring— use destructuring rather than index access:const [, value] = arrnotconst value = arr[1].id-length— identifiers must be more than one character; use_prefix for unused params (_param).- Test file naming —
*_test.js; the default filter regex is.*_test.js$. doc/plans/— gitignored; ephemeral AI-assisted implementation plans go here and are never committed.
New assertion: add to lib/core/assertion.js (instance method) and optionally a shorthand in lib/core/asserter.js.
New output format: create a subclass of Formatter, override the event methods you need, register it in FormatterFactory.
New i18n message: add the key to all four language sections in lib/i18n/translations.json, then use I18nMessage.of('key') or this.translated('key') in a formatter.
- Self-testing is mandatory — every feature or fix must be accompanied by a test in
tests/. If Testy cannot test its own change, something is wrong with the design. - One PR, one concept — no opportunistic refactors bundled in the same PR.
- Run
npm testandnpm run lintbefore opening a PR — CI will catch failures, but it's faster to catch them locally.
Applies to any session that will modify files in this repo:
- Start from a fresh
main— fetch and pull the latestmainbefore branching off work. - Default to a worktree — do the work in a git worktree unless the user explicitly says otherwise for that session.
- Ask about issue tracking — before starting, ask the user whether the task should be tracked with a GitHub issue.
- Run the test suite before touching anything — confirm
npm testis green on the starting point, so any later failure is known to be caused by the new change, not a pre-existing one. - Don't call the task done until CI is green — wait for CI checks on the PR to pass before reporting completion. If a check fails, attempt one fix; if it's still failing after that, stop and ask the user how to proceed.