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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ npm run build # compile TypeScript to dist/
npm run dev # watch mode (rebuild on changes)
npm run type-check # typecheck without emitting
npm run test # run all tests
npx vitest run src/__tests__/output.test.ts # run a single test file
npx vitest run src/lib/output.test.ts # run a single test file
npm run format # biome lint + format (auto-fix)
```

Expand Down Expand Up @@ -40,7 +40,7 @@ Each file in `src/commands/` exports a `registerXxxCommand(program)` function th

## Testing

Vitest with module mocking. Tests live in `src/__tests__/`. Common patterns:
Vitest with module mocking. Tests are colocated next to the source they cover (`foo.ts` → `foo.test.ts`); shared test fixtures live in `src/_fixtures/`. Common patterns:

- Mock `apiRequest` with `vi.mock()`
- Stub `fetch` globally for API tests
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/_fixtures/auth.ts → src/_fixtures/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MigrateAuthResult } from '@doist/cli-core/auth'
import type { OutlineAccount } from '../../lib/outline-account.js'
import type { OutlineAccount } from '../lib/outline-account.js'

/** Canonical persisted `OutlineAccount` used across auth tests. */
export const STORED_ACCOUNT: OutlineAccount = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from 'commander'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AUTH_INFO } from './_fixtures/auth.js'
import { AUTH_INFO } from '../_fixtures/auth.js'

vi.mock('../lib/auth.js', () => ({
getApiToken: async () => 'test-token',
Expand Down Expand Up @@ -48,7 +48,7 @@ async function captureAttachOptions() {
const { attachLoginCommand } = await import('@doist/cli-core/auth')
const login = new Command('login')
vi.mocked(attachLoginCommand).mockReturnValue(login)
const { registerAuthCommand } = await import('../commands/auth.js')
const { registerAuthCommand } = await import('./auth.js')
const program = new Command()
program.exitOverride()
registerAuthCommand(program)
Expand Down Expand Up @@ -245,7 +245,7 @@ describe('logTokenStorageResult', () => {

it('prints the secure-store confirmation to stdout in human mode', async () => {
const { logs, errs } = captureStreams()
const { logTokenStorageResult } = await import('../commands/auth.js')
const { logTokenStorageResult } = await import('./auth.js')

logTokenStorageResult({ storage: 'secure-store' }, 'Token stored securely', false)

Expand All @@ -255,7 +255,7 @@ describe('logTokenStorageResult', () => {

it('suppresses the stdout confirmation in machine-output mode', async () => {
const { logs } = captureStreams()
const { logTokenStorageResult } = await import('../commands/auth.js')
const { logTokenStorageResult } = await import('./auth.js')

logTokenStorageResult({ storage: 'secure-store' }, 'Token stored securely', true)

Expand All @@ -264,7 +264,7 @@ describe('logTokenStorageResult', () => {

it('routes the keyring-fallback warning to stderr (in both human and machine modes)', async () => {
const { logs, errs } = captureStreams()
const { logTokenStorageResult } = await import('../commands/auth.js')
const { logTokenStorageResult } = await import('./auth.js')

logTokenStorageResult(
{ storage: 'config-file', warning: 'system credential manager unavailable' },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Command } from 'commander'
import { describe, expect, it } from 'vitest'
import { registerChangelogCommand } from '../commands/changelog.js'
import { BaseCliError } from '../lib/errors.js'
import { formatError, formatErrorJson } from '../lib/output.js'
import { registerChangelogCommand } from './changelog.js'

describe('changelog command end-to-end', () => {
it('rejects with BaseCliError(INVALID_TYPE) when --count is not a number', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ vi.mock('@doist/cli-core/commands', () => ({
}))

import packageJson from '../../package.json' with { type: 'json' }
import { registerChangelogCommand } from '../commands/changelog.js'
import { registerChangelogCommand } from './changelog.js'

describe('changelog wrapper', () => {
beforeEach(() => {
Expand Down
22 changes: 11 additions & 11 deletions src/__tests__/commands.test.ts → src/commands/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('search command', () => {
pagination: { offset: 0, limit: 25 },
})

const { registerSearchCommand } = await import('../commands/search.js')
const { registerSearchCommand } = await import('./search.js')
const program = new Command()
program.exitOverride()
registerSearchCommand(program)
Expand Down Expand Up @@ -75,7 +75,7 @@ describe('search command', () => {
],
})

const { registerSearchCommand } = await import('../commands/search.js')
const { registerSearchCommand } = await import('./search.js')
const program = new Command()
program.exitOverride()
registerSearchCommand(program)
Expand Down Expand Up @@ -114,7 +114,7 @@ describe('document commands', () => {
},
})

const { registerDocumentCommand } = await import('../commands/document.js')
const { registerDocumentCommand } = await import('./document.js')
const program = new Command()
program.exitOverride()
registerDocumentCommand(program)
Expand All @@ -133,7 +133,7 @@ describe('document commands', () => {
pagination: { offset: 0, limit: 10 },
})

const { registerDocumentCommand } = await import('../commands/document.js')
const { registerDocumentCommand } = await import('./document.js')
const program = new Command()
program.exitOverride()
registerDocumentCommand(program)
Expand Down Expand Up @@ -186,7 +186,7 @@ describe('document commands', () => {
return Promise.reject(new Error(`Unexpected endpoint: ${endpoint}`))
})

const { registerDocumentCommand } = await import('../commands/document.js')
const { registerDocumentCommand } = await import('./document.js')
const program = new Command()
program.exitOverride()
registerDocumentCommand(program)
Expand Down Expand Up @@ -218,7 +218,7 @@ describe('document commands', () => {
errors.push(args.join(' '))
})

const { registerDocumentCommand } = await import('../commands/document.js')
const { registerDocumentCommand } = await import('./document.js')
const program = new Command()
program.exitOverride()
registerDocumentCommand(program)
Expand Down Expand Up @@ -263,7 +263,7 @@ describe('document commands', () => {
return Promise.reject(new Error(`Unexpected endpoint: ${endpoint}`))
})

const { registerDocumentCommand } = await import('../commands/document.js')
const { registerDocumentCommand } = await import('./document.js')
const program = new Command()
program.exitOverride()
registerDocumentCommand(program)
Expand Down Expand Up @@ -294,7 +294,7 @@ describe('document commands', () => {
errors.push(args.join(' '))
})

const { registerDocumentCommand } = await import('../commands/document.js')
const { registerDocumentCommand } = await import('./document.js')
const program = new Command()
program.exitOverride()
registerDocumentCommand(program)
Expand Down Expand Up @@ -326,7 +326,7 @@ describe('document commands', () => {
errors.push(args.join(' '))
})

const { registerDocumentCommand } = await import('../commands/document.js')
const { registerDocumentCommand } = await import('./document.js')
const program = new Command()
program.exitOverride()
registerDocumentCommand(program)
Expand Down Expand Up @@ -366,7 +366,7 @@ describe('document commands', () => {
errors.push(args.join(' '))
})

const { registerDocumentCommand } = await import('../commands/document.js')
const { registerDocumentCommand } = await import('./document.js')
const program = new Command()
program.exitOverride()
registerDocumentCommand(program)
Expand Down Expand Up @@ -408,7 +408,7 @@ describe('collection commands', () => {
data: [{ id: 'c1', name: 'Engineering', documentCount: 42 }],
})

const { registerCollectionCommand } = await import('../commands/collection.js')
const { registerCollectionCommand } = await import('./collection.js')
const program = new Command()
program.exitOverride()
registerCollectionCommand(program)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ vi.mock('../lib/api.js', () => ({
describeEmptyMachineOutput('ol document list', {
setup: () => {},
run: async (extraArgs) => {
const { registerDocumentCommand } = await import('../commands/document.js')
const { registerDocumentCommand } = await import('./document.js')
const program = new Command()
program.exitOverride()
registerDocumentCommand(program)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Command } from 'commander'
import { describe, expect, it, vi } from 'vitest'
import { registerUpdateCommand } from '../commands/update/index.js'
import { registerUpdateCommand } from './index.js'

// Stub out config + spinner — this test only cares about the command surface
// (subcommand names + flags) wired up via the real cli-core, so a bump to
// cli-core can't silently change `ol update`'s public CLI shape.
vi.mock('../lib/config.js', () => ({
vi.mock('../../lib/config.js', () => ({
getConfigPath: () => '/tmp/outline-cli-test/config.json',
}))

vi.mock('../lib/spinner.js', () => ({
vi.mock('../../lib/spinner.js', () => ({
withSpinner: vi.fn((_opts: unknown, fn: () => Promise<unknown>) => fn()),
}))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from 'commander'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import packageJson from '../../package.json' with { type: 'json' }
import packageJson from '../../../package.json' with { type: 'json' }

const registerCoreUpdateCommandMock = vi.fn()
const withSpinnerMock = vi.fn((_opts: unknown, fn: () => Promise<unknown>) => fn())
Expand All @@ -9,11 +9,11 @@ vi.mock('@doist/cli-core/commands', () => ({
registerUpdateCommand: registerCoreUpdateCommandMock,
}))

vi.mock('../lib/config.js', () => ({
vi.mock('../../lib/config.js', () => ({
getConfigPath: () => '/tmp/outline-cli-test/config.json',
}))

vi.mock('../lib/spinner.js', () => ({
vi.mock('../../lib/spinner.js', () => ({
withSpinner: withSpinnerMock,
}))

Expand All @@ -27,7 +27,7 @@ describe('ol update wiring', () => {
})

it('forwards packageName, currentVersion, configPath, changelogCommandName, withSpinner', async () => {
const { registerUpdateCommand } = await import('../commands/update/index.js')
const { registerUpdateCommand } = await import('./index.js')
const program = new Command()
registerUpdateCommand(program)

Expand Down
16 changes: 8 additions & 8 deletions src/__tests__/api.test.ts → src/lib/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const authMocks = vi.hoisted(() => ({
reactiveRefresh: vi.fn(async () => false),
}))

vi.mock('../lib/auth.js', () => authMocks)
vi.mock('./auth.js', () => authMocks)

vi.mock('../transport/fetch-with-retry.js', () => ({
fetchWithRetry: vi.fn(),
Expand All @@ -32,7 +32,7 @@ describe('apiRequest', () => {
const { fetchWithRetry } = await import('../transport/fetch-with-retry.js')
;(fetchWithRetry as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse)

const { apiRequest } = await import('../lib/api.js')
const { apiRequest } = await import('./api.js')
await apiRequest('documents.info', { id: 'abc' })

expect(fetchWithRetry).toHaveBeenCalledWith({
Expand All @@ -56,7 +56,7 @@ describe('apiRequest', () => {
const { fetchWithRetry } = await import('../transport/fetch-with-retry.js')
;(fetchWithRetry as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse)

const { apiRequest } = await import('../lib/api.js')
const { apiRequest } = await import('./api.js')
const result = await apiRequest('documents.list')

expect(result.data).toEqual([{ id: '1' }])
Expand All @@ -73,7 +73,7 @@ describe('apiRequest', () => {
const { fetchWithRetry } = await import('../transport/fetch-with-retry.js')
;(fetchWithRetry as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse)

const { apiRequest } = await import('../lib/api.js')
const { apiRequest } = await import('./api.js')
await expect(apiRequest('documents.list')).rejects.toThrow('Server exploded')
})

Expand All @@ -89,7 +89,7 @@ describe('apiRequest', () => {
const { fetchWithRetry } = await import('../transport/fetch-with-retry.js')
;(fetchWithRetry as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse)

const { apiRequest } = await import('../lib/api.js')
const { apiRequest } = await import('./api.js')
await expect(apiRequest('documents.list')).rejects.toThrow(
'API error: 500 Internal Server Error',
)
Expand All @@ -110,7 +110,7 @@ describe('apiRequest', () => {
.mockResolvedValueOnce('stale-token')
.mockResolvedValueOnce('rotated-token')

const { apiRequest } = await import('../lib/api.js')
const { apiRequest } = await import('./api.js')
const result = await apiRequest('documents.info', { id: 'abc' })

expect(result.data).toEqual({ id: 'ok' })
Expand All @@ -126,7 +126,7 @@ describe('apiRequest', () => {
f.mockResolvedValue({ ok: true, json: async () => ({ data: {} }) })
authMocks.proactiveRefresh.mockResolvedValueOnce('rotated-proactive')

const { apiRequest } = await import('../lib/api.js')
const { apiRequest } = await import('./api.js')
await apiRequest('documents.list')

expect(authMocks.proactiveRefresh).toHaveBeenCalledTimes(1)
Expand All @@ -144,7 +144,7 @@ describe('apiRequest', () => {
json: async () => ({ data: {} }),
})

const { apiRequest } = await import('../lib/api.js')
const { apiRequest } = await import('./api.js')
await apiRequest('documents.list')

expect(authMocks.proactiveRefresh).not.toHaveBeenCalled()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { renderError, renderSuccess } from '../lib/auth-pages.js'
import { renderError, renderSuccess } from './auth-pages.js'

describe('auth pages', () => {
it('renderSuccess returns the branded post-login page', () => {
Expand Down
Loading
Loading