feat(testing): publish shared test helpers + fixtures via ./testing#49
Conversation
Promote the genuinely-shared testing scaffolding into the published `@doist/cli-core/testing` subpath so consuming CLIs stop reinventing it: - `createTestProgram(register)` — Commander harness (was byte-identical in todoist-cli/twist-cli and inline in outline-cli) - `captureConsole(method?)` / `captureStream(stream?)` — silencing spies with onTestFinished auto-restore - `buildTokenStore` / `buildSingleEntryStore` + Ingen account fixtures + TestAccount/StoreEntry/TokenStoreHarness/MatchAccount types, moved out of the build-excluded test-support/ buildTokenStore gains an optional `matchAccount` so consumers whose stores match refs differently (twist's numeric-id/case-insensitive matcher, outline's) can mirror production semantics; it defaults to the existing id/email/label rule. Convert src/testing.ts into a src/testing/ directory and point the ./testing export at dist/testing/index. Converge cli-core's own auth suites onto the canonical helpers (drop installConsoleLogSpy/installStdoutSpy; buildProgram now wraps createTestProgram). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
doistbot
left a comment
There was a problem hiding this comment.
This PR successfully promotes valuable testing scaffolding into a published @doist/cli-core/testing subpath to eliminate duplication across the consuming CLIs. Extracting these shared utilities and converging the internal auth suites significantly standardizes our testing environment. A few adjustments are needed to address an implicit commander dependency in the barrel file, refine the captureStream mock to properly handle trailing callbacks, and fix a minor type error in the README example. Additionally, some test coverage gaps need to be filled for the new subpath exports, the custom account matcher, and the migrated modules themselves, alongside a bit of internal boilerplate deduplication.
- captureStream now detects a trailing write callback and invokes it, matching the WriteStream.write contract so write(chunk, cb) / write(chunk, enc, cb) paths don't hang - dedupe the spy lifecycle behind a private captureSpy helper - reintroduce internal installCapturedConsole/installCapturedStream wrappers in cli-harness (mirroring buildProgram over createTestProgram) so the attacher suites declare a spy once per describe instead of repeating let+beforeEach - setDefault reuses the find(ref) helper instead of duplicating the lookup - add colocated console.test.ts + program.test.ts (AGENTS.md module layout) - pin the custom matchAccount on a mutating path (setDefault) in accounts.test - extend the subpath smoke test to assert every runtime export the barrel adds - README: type-correct attachStatusCommand example; document that the whole ./testing entrypoint links commander (the barrel re-exports createTestProgram) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@doistbot /review |
doistbot
left a comment
There was a problem hiding this comment.
This PR successfully extracts our shared testing scaffolding into the published ./testing subpath, providing a centralized harness, console capturing, and account fixtures for consuming CLIs. Centralizing these canonical utilities is a great move that will significantly reduce boilerplate and duplication across our downstream tooling. A few refinements are noted for the new utilities, including queuing stream callbacks asynchronously, tightening Commander error assertions, narrowing the buildTokenStore override types to preserve contract safety, dynamically verifying exports in the smoke test, and cleaning up a brittle test alongside some redundant comments.
- captureStream queues the trailing write callback on the microtask queue instead of invoking it inline, matching the async WriteStream.write contract - narrow buildTokenStore overrides to a StoreOverrides type covering only the optional store members, so callers can't erase required methods and leave the returned store an invalid TokenStore - subpath smoke test compares the source barrel's runtime keys against the dist module's instead of a hand-maintained name list - drop the order-dependent console-restore test (passes trivially in isolation; onTestFinished is a vitest primitive) - tighten the exitOverride test to assert the commander.unknownCommand code - drop the restating JSDoc on the installCaptured* wrappers Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…usable `ReturnType<typeof vi.spyOn>` erases to a shape whose `.mock.calls` is `any`, forcing consumers to annotate `.mock.calls.map((c) => ...)` callbacks. Typing the spies as vitest's `MockInstance` restores `.mock.calls` typing so the helpers are a true drop-in for hand-rolled console/stream spies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## [0.24.0](v0.23.0...v0.24.0) (2026-05-24) ### Features * **testing:** publish shared test helpers + fixtures via ./testing ([#49](#49)) ([44b38e9](44b38e9))
|
🎉 This PR is included in version 0.24.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
## Summary - Replace bespoke inline test scaffolding with the published `@doist/cli-core/testing` helpers, mirroring [todoist-cli#361](Doist/todoist-cli#361) (per [cli-core#49](Doist/cli-core#49)). - `createTestProgram(register)` replaces inline `new Command(); exitOverride(); registerX(program)` harness blocks across `commands`, `changelog-integration`, `empty-output`, and `auth-command` tests. - `captureConsole(method?)` replaces inline `vi.spyOn(console, …)` recorders and the local `captureLogs`/`captureStreams` helpers; assertions now read the spy's `.mock.calls` via a small `lines()` accessor. - Bump `@doist/cli-core` 0.23.0 → 0.24.0, which ships these helpers. No source/runtime files changed. ## Notes / deviations - No `captureStream` usage — outline-cli has no `process.stdout/stderr.write` spies (todoist adopted it via `mockProcessStdout`). - Skipped the `buildTokenStore`/account fixtures, same as todoist-cli — outline's auth tests are module-level `vi.mock` interaction mocks, the wrong shape for cli-core's stateful store fake. - `captureConsole` takes a single method string (`captureConsole('error')`), per the published 0.24 `.d.ts`. ## Test plan - [x] `npm run type-check` - [x] `npm run lint:check` — 0 warnings, 0 errors - [x] `npm run format:check` - [x] `npm test` — 188 tests pass (21 files) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Promote the genuinely-shared testing scaffolding into the published
@doist/cli-core/testingsubpath so the consuming CLIs (outline-cli, todoist-cli, twist-cli) stop reinventing it.New
./testingexports:createTestProgram(register)— Commander harness withexitOverride(). Was byte-identical in todoist-cli/twist-cli and inline in outline-cli.captureConsole(method?)/captureStream(stream?)— silence + spy on console /process.stdout|stderr.write, auto-restoring viaonTestFinished. Returns the spy so.mock.callsassertions keep working.buildTokenStore/buildSingleEntryStore+ Ingen account fixtures (alanGrant/ellieSattler/ianMalcolm) +TestAccount/StoreEntry/TokenStoreHarness/MatchAccounttypes — moved out of the build-excludedsrc/test-support/.buildTokenStoregains an optionalmatchAccountso consumers whose stores resolve refs differently (twist's numeric-id / case-insensitive matcher, outline's) can mirror production semantics. Defaults to the existing id/email/label rule, so cli-core's own suites are unchanged.Mechanics
src/testing.ts→src/testing/directory (indexbarrel +program/console/accounts/empty-output).accounts.tsmoved fromtest-support/.package.json./testingexport now points atdist/testing/index.{js,d.ts}.installConsoleLogSpy/installStdoutSpy;buildProgramnow wrapscreateTestProgram; the 4 attacher suites moved from the describe-top-level getter pattern tobeforeEach(required becauseonTestFinishedcan't run at describe top-level).Safety
The
./testingmodule importsvitest, but it's only ever imported by test files. All three consumers build with plaintsc(no bundler) and keepvitestas a devDep, so prod code never pulls/testinginto its runtime import graph. This extends the patterndescribeEmptyMachineOutputalready used.Test plan
npm run build—dist/testing/contains index/program/console/accounts/empty-outputnpm run type-checknpm run check(oxlint + oxfmt)npm test— 499 tests pass, incl. the newaccounts.test.tsproving a no-emailaccount compiles +matchAccountoverride works, and the relocated subpath wiring test🤖 Generated with Claude Code