Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ npm install @doist/cli-core
| `options` | `ViewOptions` | Type contract for `{ json?, ndjson? }` per-command options that machine-output gates derive from. |
| `spinner` | `createSpinner` | Loading spinner factory wrapping `yocto-spinner` with disable gates. |
| `terminal` | `isCI`, `isStderrTTY`, `isStdinTTY`, `isStdoutTTY` | TTY / CI detection helpers. |
| `testing` (subpath) | `describeEmptyMachineOutput` | Vitest helpers reusable by consuming CLIs (e.g. parametrised empty-state suite covering `--json` / `--ndjson` / human modes). |
| `testing` (subpath) | `describeEmptyMachineOutput`, `createTestProgram`, `captureConsole`, `captureStream`, `buildTokenStore`, `buildSingleEntryStore`, `ingenEntries`, `alanGrant` / `ellieSattler` / `ianMalcolm`, `TestAccount` / `StoreEntry` / `TokenStoreHarness` / `MatchAccount` types | Vitest helpers + fixtures reusable by consuming CLIs: a parametrised empty-state suite (`--json` / `--ndjson` / human modes); a Commander test-program builder (`createTestProgram`; the whole subpath **requires** `commander` since the barrel re-exports it); console / stdout-stderr spies that silence + auto-restore (`captureConsole` / `captureStream`, call inside a test or `beforeEach`); and a canonical stateful in-memory `TokenStore` mock plus shared account fixtures (`buildTokenStore` / `buildSingleEntryStore`) modelling `createKeyringTokenStore`'s default-selection contract — pass `matchAccount` to mirror a consumer's own ref-matching (numeric-id / case-insensitive label). |

## Usage

Expand Down Expand Up @@ -55,6 +55,48 @@ if (tasks.length === 0) {
}
```

### Testing helpers (subpath)

Reuse the shared vitest scaffolding instead of hand-rolling it per CLI:

```ts
import { attachStatusCommand } from '@doist/cli-core/auth'
import {
alanGrant,
buildSingleEntryStore,
captureConsole,
createTestProgram,
} from '@doist/cli-core/testing'

it('greets the active account', async () => {
const logSpy = captureConsole() // silences console.log, auto-restores
const { store } = buildSingleEntryStore({ token: 'tok', account: alanGrant })
const program = createTestProgram((p) =>
attachStatusCommand(p, { store, renderText: (ctx) => `Signed in as ${ctx.account.email}` }),
)
await program.parseAsync(['node', 'cli', 'status'])
expect(logSpy).toHaveBeenCalledWith('Signed in as alan@ingen.com')
})
```

`buildTokenStore` is a stateful in-memory `TokenStore` mock. For a CLI whose store matches refs
differently from the default id/email/label rule, pass `matchAccount` so the mock resolves refs exactly
like production:

```ts
import { buildTokenStore } from '@doist/cli-core/testing'
import { matchTwistAccount } from './lib/auth-provider.js'

const { store } = buildTokenStore<TwistAccount>({
entries: [{ account: ACCOUNT_ALAN, isDefault: true }],
matchAccount: matchTwistAccount,
})
```

The whole `@doist/cli-core/testing` entrypoint links `commander` (the barrel re-exports
`createTestProgram`) and `vitest` at import time, so install both — consuming CLIs already have them —
and only import this subpath from test files.

### Markdown rendering (optional subpath)

Install the peer-deps in the consuming CLI:
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"import": "./dist/markdown.js"
},
"./testing": {
"types": "./dist/testing.d.ts",
"import": "./dist/testing.js"
"types": "./dist/testing/index.d.ts",
"import": "./dist/testing/index.js"
}
},
"scripts": {
Expand Down
12 changes: 6 additions & 6 deletions src/auth/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { describe, expect, it, vi } from 'vitest'

import { CliError } from '../errors.js'
import { formatJson, formatNdjson } from '../json.js'
import { buildProgram, installCapturedConsole } from '../test-support/cli-harness.js'
import {
type TestAccount as Account,
alanGrant,
buildTokenStore,
ellieSattler,
ingenEntries,
} from '../test-support/accounts.js'
import { buildProgram, installConsoleLogSpy } from '../test-support/cli-harness.js'
} from '../testing/accounts.js'
import {
type AttachAccountCurrentCommandOptions,
type AttachAccountListCommandOptions,
Expand Down Expand Up @@ -76,7 +76,7 @@ function buildRemove(
}

describe('attachAccountListCommand', () => {
const logSpy = installConsoleLogSpy()
const logSpy = installCapturedConsole()

it('renders default human lines with a (default) marker only on the default entry', async () => {
const { program } = buildList()
Expand Down Expand Up @@ -309,7 +309,7 @@ describe('attachAccountListCommand', () => {
})

describe('attachAccountUseCommand', () => {
const logSpy = installConsoleLogSpy()
const logSpy = installCapturedConsole()

it('calls setDefault and echoes the raw ref in the human success line', async () => {
const built = buildTokenStore()
Expand Down Expand Up @@ -425,7 +425,7 @@ describe('attachAccountUseCommand', () => {
})

describe('attachAccountCurrentCommand', () => {
const logSpy = installConsoleLogSpy()
const logSpy = installCapturedConsole()

it('renders the default human line with a (default) marker for the active account', async () => {
const { program } = buildCurrent()
Expand Down Expand Up @@ -566,7 +566,7 @@ describe('attachAccountCurrentCommand', () => {
})

describe('attachAccountRemoveCommand', () => {
const logSpy = installConsoleLogSpy()
const logSpy = installCapturedConsole()

it('removes the matched account by ref and marks it as the former default', async () => {
const built = buildTokenStore()
Expand Down
2 changes: 1 addition & 1 deletion src/auth/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
alanGrant,
buildTokenStore,
ellieSattler,
} from '../test-support/accounts.js'
} from '../testing/accounts.js'
import { type RunOAuthFlowOptions, runOAuthFlow } from './flow.js'
import type { AuthProvider, TokenBundle, TokenStore } from './types.js'

Expand Down
2 changes: 1 addition & 1 deletion src/auth/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { Command } from 'commander'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { CliError } from '../errors.js'
import { type TestAccount as Account, alanGrant } from '../test-support/accounts.js'
import { buildProgram } from '../test-support/cli-harness.js'
import { type TestAccount as Account, alanGrant } from '../testing/accounts.js'
import { attachLoginCommand } from './login.js'
import type { AuthProvider, TokenStore } from './types.js'

Expand Down
6 changes: 3 additions & 3 deletions src/auth/logout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { describe, expect, it, vi } from 'vitest'

import { CliError } from '../errors.js'
import { formatJson } from '../json.js'
import { buildProgram, installCapturedConsole } from '../test-support/cli-harness.js'
import {
type TestAccount as Account,
type TokenStoreHarness,
alanGrant,
buildSingleEntryStore,
} from '../test-support/accounts.js'
import { buildProgram, installConsoleLogSpy } from '../test-support/cli-harness.js'
} from '../testing/accounts.js'
import { attachLogoutCommand } from './logout.js'
import type { TokenStore } from './types.js'

Expand Down Expand Up @@ -44,7 +44,7 @@ function build(
}

describe('attachLogoutCommand', () => {
const logSpy = installConsoleLogSpy()
const logSpy = installCapturedConsole()

it('clears the store and emits the human success line in plain mode', async () => {
const built = buildStore()
Expand Down
6 changes: 1 addition & 5 deletions src/auth/persist.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { describe, expect, it, vi } from 'vitest'

import { CliError } from '../errors.js'
import {
type TestAccount as Account,
buildTokenStore,
ianMalcolm,
} from '../test-support/accounts.js'
import { type TestAccount as Account, buildTokenStore, ianMalcolm } from '../testing/accounts.js'
import { persistBundle } from './persist.js'
import type { TokenBundle, TokenStore } from './types.js'

Expand Down
2 changes: 1 addition & 1 deletion src/auth/providers/pkce.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'

import { type TestAccount as Account, alanGrant } from '../../test-support/accounts.js'
import { type TestAccount as Account, alanGrant } from '../../testing/accounts.js'
import { createPkceProvider } from './pkce.js'

const respond = (body: unknown, status = 200): Response =>
Expand Down
2 changes: 1 addition & 1 deletion src/auth/refresh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type TokenStoreHarness,
buildTokenStore,
ianMalcolm,
} from '../test-support/accounts.js'
} from '../testing/accounts.js'
import { refreshAccessToken } from './refresh.js'
import type {
ActiveBundleSnapshot,
Expand Down
6 changes: 3 additions & 3 deletions src/auth/status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { describe, expect, it, vi } from 'vitest'

import { CliError } from '../errors.js'
import { formatJson, formatNdjson } from '../json.js'
import { buildProgram, installCapturedConsole } from '../test-support/cli-harness.js'
import {
type TestAccount as Account,
type TokenStoreHarness,
alanGrant,
buildSingleEntryStore,
} from '../test-support/accounts.js'
import { buildProgram, installConsoleLogSpy } from '../test-support/cli-harness.js'
} from '../testing/accounts.js'
import { attachStatusCommand } from './status.js'
import type { TokenStore } from './types.js'

Expand Down Expand Up @@ -43,7 +43,7 @@ function build(
}

describe('attachStatusCommand', () => {
const logSpy = installConsoleLogSpy()
const logSpy = installCapturedConsole()

it('emits renderText output in plain mode', async () => {
const { program, renderText } = build()
Expand Down
6 changes: 3 additions & 3 deletions src/auth/token-view.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { afterEach, describe, expect, it, vi } from 'vitest'

import { CliError } from '../errors.js'
import { buildProgram, installCapturedStream } from '../test-support/cli-harness.js'
import {
type TestAccount as Account,
type TokenStoreHarness,
alanGrant,
buildSingleEntryStore,
} from '../test-support/accounts.js'
import { buildProgram, installStdoutSpy } from '../test-support/cli-harness.js'
} from '../testing/accounts.js'
import { attachTokenViewCommand } from './token-view.js'

const account = alanGrant
Expand All @@ -19,7 +19,7 @@ function buildStore(
}

describe('attachTokenViewCommand', () => {
const stdoutSpy = installStdoutSpy()
const stdoutSpy = installCapturedStream()

afterEach(() => {
vi.unstubAllEnvs()
Expand Down
Loading
Loading