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
104 changes: 99 additions & 5 deletions .claude/skills/testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ This skill covers project-specific testing patterns for the B2C CLI project.

## Running Tests

For coding agents (minimal output - only failures shown):

```bash
# Run tests - only failures + summary
pnpm run test:agent

# Run tests for specific package
pnpm --filter @salesforce/b2c-tooling-sdk run test:agent
pnpm --filter @salesforce/b2c-cli run test:agent
```

For debugging (full output with coverage):

```bash
# Run all tests with coverage
pnpm run test
Expand Down Expand Up @@ -307,6 +320,59 @@ const client = new WebDavClient(TEST_HOST, mockAuth);
const customAuth = new MockAuthStrategy('custom-token');
```

## Silencing Test Output

Commands may produce console output (tables, formatted displays) even in tests. Use these helpers to keep test output clean.

### Using runSilent for Output Capture

The `runSilent` helper uses oclif's `captureOutput` to suppress stdout/stderr:

```typescript
import { runSilent } from '../../helpers/test-setup.js';

it('returns data in non-JSON mode', async () => {
const command = new MyCommand([], {} as any);
// ... setup ...

// Silences any console output from the command
const result = await runSilent(() => command.run());

expect(result.data).to.exist;
});
```

Use `runSilent` when:
- Testing non-JSON output modes (tables, formatted displays)
- The test doesn't need to verify console output content
- You want clean test output with only pass/fail summary

### When Output Verification is Needed

If you need to verify console output, stub `ux.stdout` directly:

```typescript
import { ux } from '@oclif/core';

it('prints table in non-JSON mode', async () => {
const stdoutStub = sinon.stub(ux, 'stdout');

await command.run();

expect(stdoutStub.called).to.be.true;
});
```

### stubParse Sets Silent Logging

The `stubParse` helper automatically sets `'log-level': 'silent'` to reduce pino logger output:

```typescript
// stubParse includes silent log level by default
stubParse(command, {server: 'test.demandware.net'});
// Equivalent to: {server: 'test.demandware.net', 'log-level': 'silent'}
```

## Command Test Guidelines

Command tests should focus on **command-specific logic**, not trivial flag verification.
Expand Down Expand Up @@ -445,15 +511,43 @@ pnpm run test
open coverage/index.html
```

## Test Helpers Reference

### CLI Package (`packages/b2c-cli/test/helpers/`)

| Helper | Purpose |
|--------|---------|
| `runSilent(fn)` | Capture and suppress stdout/stderr from command execution |
| `stubParse(command, flags, args)` | Stub oclif's parse method with flags (includes silent log level) |
| `createTestCommand(CommandClass, config, flags, args)` | Create command instance with stubbed parse |
| `createIsolatedConfigHooks()` | Mocha hooks for config isolation |
| `createIsolatedEnvHooks()` | Mocha hooks for env var isolation |

### SDK Package (`packages/b2c-tooling-sdk/test/helpers/`)

| Helper | Purpose |
|--------|---------|
| `MockAuthStrategy` | Mock authentication for API clients |
| `stubParse(command, flags, args)` | Stub oclif's parse method (includes silent log level) |
| `createNullStream()` | Create a writable stream that discards output |
| `CapturingStream` | Writable stream that captures output for assertions |

### SDK Test Utils (exported from package)

```typescript
import { isolateConfig, restoreConfig } from '@salesforce/b2c-tooling-sdk/test-utils';
```

## Writing Tests Checklist

1. Create test file in `test/` mirroring source structure
2. Use `.test.ts` suffix
3. Import from package names, not relative paths
4. Set up MSW server for HTTP tests (avoid fake timers)
5. Use `isolateConfig()`/`restoreConfig()` for config-dependent tests
6. Use `pollInterval` option for polling operations
7. Use MockAuthStrategy for authenticated clients
8. Test both success and error paths
9. Focus on command-specific logic, not trivial delegation
10. Run tests: `pnpm --filter <package> run test`
6. Use `runSilent()` for commands that produce console output
7. Use `pollInterval` option for polling operations
8. Use MockAuthStrategy for authenticated clients
9. Test both success and error paths
10. Focus on command-specific logic, not trivial delegation
11. Run tests: `pnpm --filter <package> run test`
36 changes: 32 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,41 @@ pnpm run build
pnpm --filter @salesforce/b2c-cli run build
pnpm --filter @salesforce/b2c-tooling-sdk run build

# Run tests (includes linting)
pnpm run test

# Dev mode for CLI (uses source files directly)
pnpm --filter @salesforce/b2c-cli run dev
# or using convenience script
./cli
```

## Commands for Coding Agents

These commands produce condensed output optimized for AI coding agents:

```bash
# Run tests (minimal output - only failures + summary)
pnpm run test:agent

# Run tests for specific package
pnpm --filter @salesforce/b2c-cli run test:agent
pnpm --filter @salesforce/b2c-tooling-sdk run test:agent

# Lint (errors only, no warnings)
pnpm run lint:agent

# Type-check (single-line errors, no color)
pnpm run typecheck:agent

# Format check (lists only files needing formatting)
pnpm run -r format:check
```

## Verbose Commands (Debugging/CI)

Use these for detailed output during debugging or in CI pipelines:

```bash
# Run tests with full output and coverage
pnpm run test

# Run tests for specific package
pnpm --filter @salesforce/b2c-cli run test
Expand All @@ -33,7 +61,7 @@ pnpm --filter @salesforce/b2c-tooling-sdk run test
# Format code with prettier
pnpm run -r format

# Lint only (without tests)
# Lint with full output
pnpm run -r lint
```

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
"start": "pnpm --filter @salesforce/b2c-cli run dev",
"test": "pnpm -r test",
"test:unit": "pnpm -r run test:unit",
"test:agent": "pnpm -r run test:agent",
"coverage": "pnpm -r run coverage",
"format": "pnpm -r run format",
"lint": "pnpm -r run lint",
"lint:agent": "pnpm -r run lint:agent",
"typecheck:agent": "pnpm -r run typecheck:agent",
"build": "pnpm -r run build",
"docs:api": "typedoc",
"docs:dev": "pnpm run docs:api && vitepress dev docs",
Expand Down
3 changes: 3 additions & 0 deletions packages/b2c-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@
"scripts": {
"build": "shx rm -rf dist && tsc -p tsconfig.build.json",
"lint": "eslint",
"lint:agent": "eslint --quiet",
"typecheck:agent": "tsc --noEmit --pretty false",
"format": "prettier --write src",
"format:check": "prettier --check src",
"postpack": "shx rm -f oclif.manifest.json",
Expand All @@ -262,6 +264,7 @@
"test": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
"test:ci": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"",
"test:unit": "env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
"test:agent": "env OCLIF_TEST_ROOT=. mocha --forbid-only --reporter min --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"",
"test:e2e": "env OCLIF_TEST_ROOT=. mocha --forbid-only --reporter json --reporter-option output=test-results.json \"test/functional/e2e/**/*.test.ts\"",
"coverage": "c8 report",
"version": "oclif readme && git add README.md",
Expand Down
4 changes: 3 additions & 1 deletion packages/b2c-cli/test/commands/_test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import {expect} from 'chai';

describe('_test', () => {
it('runs the smoke test command without errors', async () => {
// Skip in automated tests - this is a debug command that intentionally produces log output
// Run manually with: ./cli _test
it.skip('runs the smoke test command without errors', async () => {

Check warning on line 12 in packages/b2c-cli/test/commands/_test/index.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

Unexpected skipped mocha test

Check warning on line 12 in packages/b2c-cli/test/commands/_test/index.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected skipped mocha test
const {error} = await runCommand('_test');
expect(error).to.be.undefined;
});
Expand Down
6 changes: 3 additions & 3 deletions packages/b2c-cli/test/commands/docs/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {expect} from 'chai';
import {afterEach, beforeEach} from 'mocha';
import sinon from 'sinon';
import DocsSearch from '../../../src/commands/docs/search.js';
import {createIsolatedConfigHooks, createTestCommand} from '../../helpers/test-setup.js';
import {createIsolatedConfigHooks, createTestCommand, runSilent} from '../../helpers/test-setup.js';

describe('docs search', () => {
const hooks = createIsolatedConfigHooks();
Expand Down Expand Up @@ -43,7 +43,7 @@ describe('docs search', () => {
const listStub = sinon.stub().returns([{id: 'a', title: 'A', filePath: 'a.md'}]);
command.operations = {...command.operations, listDocs: listStub};

const result = await command.run();
const result = await runSilent(() => command.run());

expect(result.entries).to.have.length(1);
});
Expand All @@ -69,7 +69,7 @@ describe('docs search', () => {
const searchStub = sinon.stub().returns([{entry: {id: 'a', title: 'A', filePath: 'a.md'}, score: 0.1}]);
command.operations = {...command.operations, searchDocs: searchStub};

const result = await command.run();
const result = await runSilent(() => command.run());

expect(result.results).to.have.length(1);
});
Expand Down
4 changes: 2 additions & 2 deletions packages/b2c-cli/test/commands/ecdn/security/get.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {expect} from 'chai';
import {afterEach, beforeEach} from 'mocha';
import sinon from 'sinon';
import EcdnSecurityGet from '../../../../src/commands/ecdn/security/get.js';
import {createIsolatedConfigHooks, createTestCommand} from '../../../helpers/test-setup.js';
import {createIsolatedConfigHooks, createTestCommand, runSilent} from '../../../helpers/test-setup.js';

/**
* Unit tests for eCDN security get command CLI logic.
Expand Down Expand Up @@ -98,7 +98,7 @@ describe('ecdn security get', () => {
}),
});

const result = await command.run();
const result = await runSilent(() => command.run());

expect(result.settings.securityLevel).to.equal('high');
expect(result.settings.wafEnabled).to.be.true;
Expand Down
4 changes: 2 additions & 2 deletions packages/b2c-cli/test/commands/ecdn/zones/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {expect} from 'chai';
import {afterEach, beforeEach} from 'mocha';
import sinon from 'sinon';
import EcdnZonesList from '../../../../src/commands/ecdn/zones/list.js';
import {createIsolatedConfigHooks, createTestCommand} from '../../../helpers/test-setup.js';
import {createIsolatedConfigHooks, createTestCommand, runSilent} from '../../../helpers/test-setup.js';

/**
* Unit tests for eCDN zones list command CLI logic.
Expand Down Expand Up @@ -129,7 +129,7 @@ describe('ecdn zones list', () => {
}),
});

const result = await command.run();
const result = await runSilent(() => command.run());

expect(result).to.have.property('total', 1);
expect(result.zones).to.have.lengthOf(1);
Expand Down
5 changes: 3 additions & 2 deletions packages/b2c-cli/test/commands/ods/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {expect} from 'chai';
import sinon from 'sinon';
import OdsCreate from '../../../src/commands/ods/create.js';
import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils';
import {runSilent} from '../../helpers/test-setup.js';

function stubCommandConfigAndLogger(command: any, sandboxApiHost = 'admin.dx.test.com'): void {
Object.defineProperty(command, 'config', {
Expand Down Expand Up @@ -199,7 +200,7 @@ describe('ods create', () => {
}),
});

const result = await command.run();
const result = await runSilent(() => command.run());

expect(result.id).to.equal('sb-123');
});
Expand Down Expand Up @@ -257,7 +258,7 @@ describe('ods create', () => {
},
});

await command.run();
await runSilent(() => command.run());

expect(requestBody.settings).to.be.undefined;
});
Expand Down
3 changes: 2 additions & 1 deletion packages/b2c-cli/test/commands/ods/get.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import sinon from 'sinon';

import OdsGet from '../../../src/commands/ods/get.js';
import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils';
import {runSilent} from '../../helpers/test-setup.js';

function stubCommandConfigAndLogger(command: any, sandboxApiHost = 'admin.dx.test.com'): void {
Object.defineProperty(command, 'config', {
Expand Down Expand Up @@ -134,7 +135,7 @@ describe('ods get', () => {
}),
});

const result = await command.run();
const result = await runSilent(() => command.run());

// Command returns the sandbox data regardless of JSON mode
expect(result.id).to.equal('sandbox-123');
Expand Down
3 changes: 2 additions & 1 deletion packages/b2c-cli/test/commands/ods/info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import sinon from 'sinon';

import OdsInfo from '../../../src/commands/ods/info.js';
import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils';
import {runSilent} from '../../helpers/test-setup.js';

function stubCommandConfigAndLogger(command: any, sandboxApiHost = 'admin.dx.test.com'): void {
Object.defineProperty(command, 'config', {
Expand Down Expand Up @@ -146,7 +147,7 @@ describe('ods info', () => {
throw new Error(`Unexpected path: ${path}`);
});

const result = await command.run();
const result = await runSilent(() => command.run());

expect(result).to.have.property('user');
expect(result).to.have.property('system');
Expand Down
5 changes: 3 additions & 2 deletions packages/b2c-cli/test/commands/ods/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {expect} from 'chai';
import sinon from 'sinon';
import OdsList from '../../../src/commands/ods/list.js';
import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils';
import {runSilent} from '../../helpers/test-setup.js';

function stubCommandConfigAndLogger(command: any, sandboxApiHost = 'admin.dx.test.com'): void {
Object.defineProperty(command, 'config', {
Expand Down Expand Up @@ -179,7 +180,7 @@ describe('ods list', () => {
}),
});

const result = await command.run();
const result = await runSilent(() => command.run());

// Command returns data regardless of JSON mode
expect(result).to.have.property('count', 1);
Expand Down Expand Up @@ -241,7 +242,7 @@ describe('ods list', () => {
}),
});

const result = await command.run();
const result = await runSilent(() => command.run());

expect(result.count).to.equal(0);
expect(result.data).to.deep.equal([]);
Expand Down
5 changes: 3 additions & 2 deletions packages/b2c-cli/test/commands/setup/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import sinon from 'sinon';

import SetupConfig from '../../../src/commands/setup/config.js';
import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils';
import {runSilent} from '../../helpers/test-setup.js';
import type {ConfigSourceInfo, NormalizedConfig} from '@salesforce/b2c-tooling-sdk/config';

/* eslint-disable @typescript-eslint/no-explicit-any */
Expand Down Expand Up @@ -341,7 +342,7 @@ describe('setup config', () => {
],
);

const result = await command.run();
const result = await runSilent(() => command.run());

expect(result).to.have.property('config');
expect(result.config.hostname).to.equal('test.example.com');
Expand All @@ -360,7 +361,7 @@ describe('setup config', () => {

stubResolvedConfig(command, {hostname: 'test.example.com'});

await command.run();
await runSilent(() => command.run());

expect(warnings).to.include('Sensitive values are displayed unmasked.');
});
Expand Down
Loading
Loading