diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md index 41143e4..f5c1f87 100644 --- a/.cursor/rules/README.md +++ b/.cursor/rules/README.md @@ -1,25 +1,5 @@ -# Cursor rules +# Cursor (optional) -Rules give context-aware guidance for this **Contentstack query-export CLI plugin** (`@contentstack/cli-cm-export-query`). +**Cursor** users: start at **[AGENTS.md](../../AGENTS.md)**. All conventions live in **`skills/*/SKILL.md`**. -## Rules overview - -| File | Purpose | -|------|---------| -| `dev-workflow.md` | TDD, structure, validation commands (always applied) | -| `typescript.mdc` | TypeScript style and naming | -| `contentstack-cli.mdc` | Contentstack CLI utilities, export flow, API habits | -| `testing.mdc` | Mocha/Chai/Sinon and coverage | -| `oclif-commands.mdc` | Command flag and delegation patterns | - -## How they attach - -- **Always**: `dev-workflow.md` -- **TypeScript**: `typescript.mdc` -- **Commands** (`src/commands/**`): `oclif-commands.mdc` + `typescript.mdc` -- **Core / utils** (`src/core/**`, `src/utils/**`, `src/types/**`): `contentstack-cli.mdc` + `typescript.mdc` -- **Tests** (`test/**`): `testing.mdc` + domain rules as needed - -## Chat shortcuts - -You can `@`-mention rule topics (for example TypeScript or testing) depending on how your workspace maps rule names. +This folder only points contributors to **`AGENTS.md`** so editor-specific config does not duplicate the canonical docs. diff --git a/.cursor/rules/contentstack-cli.mdc b/.cursor/rules/contentstack-cli.mdc deleted file mode 100644 index c4be6e1..0000000 --- a/.cursor/rules/contentstack-cli.mdc +++ /dev/null @@ -1,39 +0,0 @@ ---- -description: "Contentstack query-export plugin: CLI utilities, export flow, and API habits" -globs: - - "**/core/**/*.ts" - - "**/utils/**/*.ts" - - "**/types/**/*.ts" - - "**/config/**/*.ts" -alwaysApply: false ---- - -# Contentstack CLI — query export plugin - -This package is **`@contentstack/cli-cm-export-query`**. Business logic lives in **`src/core/`** and **`src/utils/`** (there is no `src/services/` layer). - -## Stack and auth - -- Use **`@contentstack/cli-utilities`**: `managementSDKClient`, `flags`, `log`, `handleAndLogError`, `sanitizePath`, etc. -- Resolve management tokens and stack context the same way as **`src/commands/cm/stacks/export-query.ts`** (alias, `--stack-api-key`, config file). -- **Never** log or write secrets (API keys, management tokens) to export artifacts or casual log lines. - -## Export orchestration - -- **`QueryExporter`** (`src/core/query-executor.ts`) drives the flow: parse query → export modules → resolve dependencies / references / assets per flags. -- **`ModuleExporter`** (`src/core/module-exporter.ts`) handles module-level export alongside **`@contentstack/cli-cm-export`** behavior. -- Prefer extending **utils** and **core** classes rather than putting heavy logic in the command’s `run()`. - -## Queries - -- Queries are JSON (string or path to a JSON file); see **`QueryParser`** (`src/utils/query-parser.ts`) and command examples in `export-query.ts`. -- Respect flags: **`skip-references`**, **`skip-dependencies`**, **`secured-assets`**, branch / branch-alias. - -## API usage - -- Respect Contentstack **rate limits**; use backoff for **429** and transient failures when adding new API calls. -- Mock **all** HTTP/SDK usage in tests (sinon); no live stack calls in unit tests. - -## Not in this plugin - -- Other CLI repos may use **`BaseBulkCommand`** or **`bulk-operation/*.json`** logs. This plugin uses **`Command`** from **`@contentstack/cli-command`** and the **`QueryExporter`** pipeline only. diff --git a/.cursor/rules/dev-workflow.md b/.cursor/rules/dev-workflow.md deleted file mode 100644 index fbfd52a..0000000 --- a/.cursor/rules/dev-workflow.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -description: "Core development workflow and TDD patterns - always applied" -globs: ["**/*.ts", "**/*.js", "**/*.json"] -alwaysApply: true ---- - -# Development Workflow - -## Quick Reference - -For detailed patterns, see project skills: - -- `@skills/testing` — Testing and TDD -- `@skills/contentstack-cli` — Export command, utilities, and API usage -- `@skills/framework` — Config, logging, and shared patterns -- `@skills/code-review` — PR review checklist - -## TDD workflow (recommended) - -For **new behavior or bug fixes**, prefer working in three steps: - -1. **RED** → Write a failing test (or extend an existing one) -2. **GREEN** → Minimal code to pass -3. **REFACTOR** → Clean up while tests stay green - -**Exceptions (no new test required when behavior is unchanged):** pure refactors, documentation-only edits, comments/config-only tweaks, trivial typos. - -## Guidelines - -- **Coverage:** aim high; **~80%** (lines/branches/functions) is an **aspirational target**, not a CI gate in this repo -- **TypeScript** — explicit return types where practical; avoid `any` -- **NO test.skip or .only** in commits - -## File structure (this repo) - -- **Command**: `src/commands/cm/stacks/export-query.ts` -- **Core**: `src/core/` — `QueryExporter`, `ModuleExporter` -- **Utils**: `src/utils/` — query parsing, config, dependencies, assets, branches, file helpers -- **Types**: `src/types/index.ts` -- **Config**: `src/config/` (copied to `lib/` on build) -- **Messages**: `messages/index.json` -- **Tests**: `test/unit/*.test.ts` (grouped by module under test) - -## Naming conventions - -- Files: `kebab-case.ts` / `kebab-case.test.ts` -- Classes: `PascalCase` -- Functions: `camelCase` -- Tests: `should [behavior] when [condition]` - -## Before coding - -1. Read relevant `@skills/*` references -2. For behavior changes: prefer a failing test first, then implement -3. Refactor and run tests - -## Validation commands - -- `npm run lint` — ESLint on `src/**/*.ts` -- `npm run test` — All tests with nyc -- `npm run test:report` — LCOV coverage report -- `npm run test:unit` — Unit tests only -- `npm run format` — ESLint `--fix` - -## Commit suggestions - -- Conventional commits are helpful but optional: `feat(scope): description` -- Tests passing before merge -- No lint errors -- No stray `console.log` / `debugger` diff --git a/.cursor/rules/oclif-commands.mdc b/.cursor/rules/oclif-commands.mdc deleted file mode 100644 index 6349d83..0000000 --- a/.cursor/rules/oclif-commands.mdc +++ /dev/null @@ -1,45 +0,0 @@ ---- -description: "OCLIF / Contentstack command patterns for this plugin" -globs: ["**/commands/**/*.ts"] -alwaysApply: false ---- - -# Command standards (`export-query`) - -This plugin uses **`Command`** from **`@contentstack/cli-command`** and **`flags` / `FlagInput`** from **`@contentstack/cli-utilities`** (not `@oclif/core` `Flags` directly). Follow **`src/commands/cm/stacks/export-query.ts`** as the source of truth. - -## Structure - -```typescript -import { Command } from '@contentstack/cli-command'; -import { flags, FlagInput, managementSDKClient, log, handleAndLogError } from '@contentstack/cli-utilities'; -import { QueryExporter } from '../../../core/query-executor'; - -export default class ExportQueryCommand extends Command { - static description = 'Export content from a stack using query-based filtering'; - static examples = ['csdx cm:stacks:export-query --query \'{"modules":{...}}\'']; - - static flags: FlagInput = { - query: flags.string({ required: true, description: 'Query as JSON string or file path' }), - }; - - async run(): Promise { - const { flags } = await this.parse(ExportQueryCommand); - // Build config via src/utils (e.g. setupQueryExportConfig), then: - const exporter = new QueryExporter(client, exportQueryConfig); - await exporter.execute(); - } -} -``` - -## Practices - -- **Validate early**: required `query`; ensure stack auth is present before calling the API. -- **Delegate**: keep parsing, paths, and SDK wiring out of `QueryExporter` where helpers already exist under **`src/utils/`**. -- **User feedback**: prefer **`log`** from **`@contentstack/cli-utilities`** with export context. -- **Errors**: use **`handleAndLogError`** and/or command error UX consistent with other Contentstack CLI plugins. - -## Flags - -- Define flags with **`flags.string`**, **`flags.boolean`**, etc., and **`FlagInput`** typing for the static `flags` object. -- Use **`exclusive`** on mutually exclusive options (see branch vs branch-alias in `export-query.ts`). diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc deleted file mode 100644 index e1db60f..0000000 --- a/.cursor/rules/testing.mdc +++ /dev/null @@ -1,66 +0,0 @@ ---- -description: "Testing patterns and TDD workflow" -globs: ["**/__tests__/**/*.ts", "**/*.spec.ts", "**/*.test.ts"] -alwaysApply: true ---- - -# Testing Standards - -## TDD workflow -1. For behavior changes, prefer a failing test first -2. Minimal code to pass -3. Refactor while keeping tests green - -Pure refactors and docs-only changes may skip new tests when behavior is unchanged. - -## Coverage -- **~80%** (lines, branches, functions) is an **aspirational** goal, not enforced as a hard gate here -- Test both success and failure paths -- Mock all external dependencies - -## Test Patterns -```typescript -// ✅ GOOD - Simple test structure -describe('[ComponentName]', () => { - beforeEach(() => { - // Setup mocks and test data - sinon.stub(ExternalService.prototype, 'method').resolves(mockData); - }); - - afterEach(() => { - sinon.restore(); - }); - - it('should [expected behavior] when [condition]', () => { - // Arrange - const input = { /* test data */ }; - - // Act - const result = component.method(input); - - // Assert - expect(result).to.equal(expectedOutput); - }); -}); -``` - -## Mocking Standards -- Use sinon for API response mocking -- Never make real API calls in tests -- Mock at module boundaries (SDK clients, `fsUtil`, etc.), not irrelevant internals - -## Layout - -- Unit tests live under **`test/unit/**/*.test.ts`** (Mocha). - -## Common mock patterns -```typescript -// Mock external API -sinon.stub(HttpClient.prototype, 'request').resolves(mockResponse); - -// Mock file operations -sinon.stub(fs, 'readFile').resolves(mockFileContent); - -// Mock rate limiter -sinon.stub(RateLimiter.prototype, 'wait').resolves(); -``` \ No newline at end of file diff --git a/.cursor/rules/typescript.mdc b/.cursor/rules/typescript.mdc deleted file mode 100644 index 4b982c9..0000000 --- a/.cursor/rules/typescript.mdc +++ /dev/null @@ -1,49 +0,0 @@ ---- -description: "TypeScript strict mode standards and naming conventions" -globs: ["**/*.ts", "**/*.tsx"] -alwaysApply: false ---- - -# TypeScript Standards - -## Strict Mode Requirements -- Explicit return types for all functions -- No `any` type usage -- Strict null checks enabled -- No unused variables or imports - -## Naming Conventions -- **Files**: kebab-case.ts -- **Classes**: PascalCase -- **Functions/Variables**: camelCase -- **Constants**: SCREAMING_SNAKE_CASE -- **Interfaces**: PascalCase (no I prefix) -- **Types**: PascalCase - -## Code Examples -```typescript -// ✅ GOOD - Explicit types and proper naming -export class UserService { - async fetchUser(userId: string): Promise { - const user = await this.client.getUser(userId); - return user; - } -} - -// ❌ BAD - Implicit any, poor naming -export class userservice { - async fetchUser(id) { - return await this.client.getUser(id); - } -} -``` - -## Import Organization -1. Node.js built-ins -2. External libraries -3. Internal modules (relative imports last) - -## Error Handling -- Use custom error classes -- Include error context and cause -- Never swallow errors silently \ No newline at end of file diff --git a/.cursor/skills/SKILL.md b/.cursor/skills/SKILL.md deleted file mode 100644 index d63c12d..0000000 --- a/.cursor/skills/SKILL.md +++ /dev/null @@ -1,5 +0,0 @@ -# Project skills - -Agent-oriented skills for **cli-query-export** (`@contentstack/cli-cm-export-query`) live under **[`skills/`](../../skills/)**. - -Reference in chat with `@skills/` (for example `@skills/contentstack-cli`, `@skills/testing`). diff --git a/.talismanrc b/.talismanrc index ff4fcbf..cc79d8e 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,14 +1,14 @@ fileignoreconfig: - - filename: skills/code-review/SKILL.md - checksum: 8c0c5c1a18ba08aa0c048d261f1ca81c152def23745fd74b531b75a23a5ac969 - - filename: skills/framework/SKILL.md - checksum: 5674849276e2087f6361decb2c7e427f451ec7799538fa2eb697b0a8b7b92f44 - - filename: .cursor/rules/contentstack-cli.mdc - checksum: 11bf8883f584b900ce1cde336057391717b47eb5a304cb46c5660bb3185cef2f - - filename: skills/code-review/references/code-review-checklist.md - checksum: 6e65ad06469083ed0196edaf8bd2d4478800493b32535be7c98e436082fba44a - - filename: skills/framework/references/framework-patterns.md - checksum: cae3858eea36c1f716ebe4a9679fc3d4eee628cb244cf4fc0a6eccbd8cecb36d - filename: package-lock.json - checksum: 9b73c1e977f09964843bd5f7529ca8decb7e8b5ab462a4e9ab167ff2a05df53f -version: '1.0' + checksum: a907736828db3f054d2e29324a26265e4eeee28416ca2feef4b71ac037468d08 + - filename: skills/testing/SKILL.md + checksum: da9831797a5e6a4d2e6e846c3f6d2583d84008d2dfd454dd7effe2f897c43a7b + - filename: skills/framework/SKILL.md + checksum: 2b74c9aaeeee804fe3c2d11e5ed14a20b82922c1d343e7ac0572da1abcc6d26a + - filename: skills/contentstack-cli/SKILL.md + checksum: b42e3526fb902a31080824b776cc8e233646139ee0915d89c0925744d56d586f + - filename: skills/code-review/SKILL.md + checksum: dd24d9e45419787c36cd50142422be53316c4e88dde2d651cc78314db1d4e6aa + - filename: skills/dev-workflow/SKILL.md + checksum: 03434201e3aaaa9239aff2f97826d64ac4ca31467b77c07330ac4b608ee24939 +version: "1.0" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a50993c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,45 @@ +# CLI export-query plugin – Agent guide + +**Universal entry point** for contributors and AI agents. Detailed conventions live in **`skills/*/SKILL.md`**. + +## What this repo is + +| Field | Detail | +| --- | --- | +| **Name:** | `@contentstack/cli-cm-export-query` ([repository](https://github.com/contentstack/cli)) | +| **Purpose:** | OCLIF plugin for **query-based** stack export (`cm:stacks:export-query` / short `EXPRTQRY`); implements `QueryExporter` and related export flow. | +| **Out of scope (if any):** | Other export/import plugins live in sibling packages; this repo is only the query-export plugin. | + +## Tech stack (at a glance) + +| Area | Details | +| --- | --- | +| **Language** | TypeScript **^4.9** (`tsconfig.json`); Node **>= 14** (`engines`) | +| **Build** | `tsc -b` → `lib/`; `prepack` runs compile + `oclif manifest` + `oclif readme`; copies `src/config` → `lib/` | +| **Tests** | Mocha + Chai + Sinon; **nyc** coverage; tests under `test/**/*.test.ts` (see [skills/testing/SKILL.md](skills/testing/SKILL.md)) | +| **Lint / coverage** | ESLint `src/**/*.ts`; nyc in `npm test` | +| **Other** | OCLIF v4, Husky | + +## Commands (quick reference) + +| Command type | Command | +| --- | --- | +| **Build** | `npm run build` | +| **Test** | `npm test` | +| **Lint** | `npm run lint` | + +CI: [.github/workflows/unit-test.yml](.github/workflows/unit-test.yml); also `release.yml`, `sca-scan.yml`, `policy-scan.yml` under [.github/workflows/](.github/workflows/). + +## Where the documentation lives: skills + +| Skill | Path | What it covers | +| --- | --- | --- | +| Development workflow | [skills/dev-workflow/SKILL.md](skills/dev-workflow/SKILL.md) | CI, branches, Husky, PR expectations | +| Contentstack CLI | [skills/contentstack-cli/SKILL.md](skills/contentstack-cli/SKILL.md) | Commands, `QueryExporter`, APIs | +| Framework | [skills/framework/SKILL.md](skills/framework/SKILL.md) | Config, logging, errors, utilities | +| Testing | [skills/testing/SKILL.md](skills/testing/SKILL.md) | Mocha/Chai/Sinon, nyc, TDD | +| Code review | [skills/code-review/SKILL.md](skills/code-review/SKILL.md) | PR checklist | + +## Using Cursor (optional) + +If you use **Cursor**, [.cursor/rules/README.md](.cursor/rules/README.md) only points to **`AGENTS.md`**—same docs as everyone else. diff --git a/skills/README.md b/skills/README.md index 19a19d6..7811571 100644 --- a/skills/README.md +++ b/skills/README.md @@ -1,24 +1,3 @@ -# Skills +# Skills – CLI export-query -Reusable agent guidance for **`@contentstack/cli-cm-export-query`** (query-based stack export). Use with any tool that supports file references. - -## Quick reference - -| Skill | Use when | -|-------|----------| -| **contentstack-cli** | Command, `QueryExporter`, utilities, Contentstack APIs | -| **testing** | Mocha, Chai, Sinon, TDD, coverage | -| **framework** | Config, logging, errors, shared utilities | -| **code-review** | PR / change review | - -## How to reference - -``` -Follow @skills/contentstack-cli and @skills/testing for this change. -``` - -## Project context - -- **Stack:** TypeScript, OCLIF (via `@contentstack/cli-command`), Contentstack CLI utilities, Mocha / Chai / Sinon, nyc -- **Layout:** `src/commands/` → `src/core/` (`QueryExporter`, `ModuleExporter`) → `src/utils/` -- **Tests:** `test/unit/**/*.test.ts` +Source of truth for detailed guidance. Read [AGENTS.md](../AGENTS.md) for the skill index, then open the `SKILL.md` that matches your task. Each folder contains `SKILL.md` with YAML frontmatter (`name`, `description`). diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md index 160c8e7..ad0a78e 100644 --- a/skills/code-review/SKILL.md +++ b/skills/code-review/SKILL.md @@ -5,28 +5,9 @@ description: PR review checklist for this repo and similar CLI plugins. Use when # Code Review Skill -## Quick Reference +Use the **Quick checklist template** for a short PR paste. Numbered sections **1**–**7** and **Review checklist summary** below are the full deep review for **`@contentstack/cli-cm-export-query`** and related CLI work. -For comprehensive review guidelines, see: -- **[Code Review Checklist](./references/code-review-checklist.md)** - Complete PR review guidelines with severity levels and checklists - -## Review Process - -### Severity Levels -- 🔴 **Critical**: Must fix before merge (security, correctness, breaking changes) -- 🟡 **Important**: Should fix (performance, maintainability, best practices) -- 🟢 **Suggestion**: Consider improving (style, optimization, readability) - -### Quick Review Categories - -1. **Security** - No hardcoded secrets, input validation, secure error handling -2. **Correctness** - Logic validation, error scenarios, data integrity -3. **Architecture** - Code organization, design patterns, modularity -4. **Performance** - Efficiency, resource management, concurrency -5. **Testing** - Test coverage (aspirational targets), quality tests, sensible TDD when adding behavior -6. **Conventions** - TypeScript standards, code style, documentation - -## Quick Checklist Template +## Quick checklist template ```markdown ## Security Review @@ -60,6 +41,164 @@ For comprehensive review guidelines, see: - [ ] Documentation adequate ``` -## Usage - -This skill provides Cursor-specific PR review automation. Reference the universal code review checklist above for detailed guidelines, common issues, and review best practices that work with any AI agent. \ No newline at end of file +## 1. Security Review + +### Authentication & Authorization +- [ ] No hardcoded API keys, tokens, or credentials +- [ ] Sensitive data not logged to console or files +- [ ] Proper token validation and expiration handling +- [ ] Environment variables used for secrets + +### Input Validation +- [ ] All user inputs validated and sanitized +- [ ] Command flags properly validated +- [ ] File paths sanitized to prevent directory traversal +- [ ] API responses validated before processing + +### Error Handling +- [ ] Errors don't expose sensitive information +- [ ] Stack traces filtered in production +- [ ] Proper error logging without secrets + +## 2. Correctness Review + +### Logic Validation +- [ ] Business logic correctly implemented +- [ ] Edge cases handled appropriately +- [ ] Null/undefined checks in place +- [ ] Async operations properly awaited + +### Error Scenarios +- [ ] Network failures handled gracefully +- [ ] Rate limiting respected and handled +- [ ] Partial failures in batch operations managed +- [ ] Retry logic implemented correctly + +### Data Integrity +- [ ] Data transformations are reversible where needed +- [ ] Batch operations maintain consistency +- [ ] Rollback mechanisms for critical operations +- [ ] Proper validation before destructive operations + +## 3. Architecture Review + +### Code Organization +- [ ] Proper separation of concerns (Commands → Services → Utils) +- [ ] Single responsibility principle followed +- [ ] Dependencies injected, not hardcoded +- [ ] Interfaces used for abstractions + +### Design Patterns +- [ ] Consistent error handling patterns +- [ ] Proper use of async/await +- [ ] Service layer properly abstracted +- [ ] Configuration management centralized + +### Modularity +- [ ] Functions are focused and testable +- [ ] Classes have clear responsibilities +- [ ] Modules are loosely coupled +- [ ] Common functionality extracted to utilities + +## 4. Performance Review + +### Efficiency +- [ ] Unnecessary API calls eliminated +- [ ] Export/query work batched or paginated where the Management API requires it +- [ ] Proper pagination implemented when listing large result sets +- [ ] Rate limiting respected + +### Resource Management +- [ ] Memory usage optimized for large datasets +- [ ] File handles properly closed +- [ ] Network connections cleaned up +- [ ] No memory leaks in long-running operations + +### Concurrency +- [ ] Appropriate concurrency limits set +- [ ] Race conditions avoided +- [ ] Deadlocks prevented +- [ ] Resource contention minimized + +## 5. Testing Review + +### Test Coverage +- [ ] All new/modified code has tests +- [ ] Both success and failure paths tested +- [ ] Edge cases covered +- [ ] Integration tests for complex workflows + +### Test Quality +- [ ] Tests are focused and readable +- [ ] Proper mocking of external dependencies +- [ ] No real API calls in tests +- [ ] Test data is realistic and maintainable + +### TDD / test discipline +- [ ] New behavior covered by tests where practical (test-first preferred, not mandatory for refactors/docs) +- [ ] Tests fail appropriately when code is broken +- [ ] Tests are independent and can run in any order +- [ ] No test.skip or .only in committed code + +## 6. CLI-Specific Review + +### OCLIF Command Structure +- [ ] Extends appropriate base command class +- [ ] Proper flag definitions with validation +- [ ] Clear command description and examples +- [ ] Appropriate error handling with user-friendly messages + +### User Experience +- [ ] Progress indicators for long operations +- [ ] Clear success/failure messaging +- [ ] Proper use of colors and formatting +- [ ] Confirmation prompts for destructive actions + +### Command patterns +- [ ] Input validation before processing +- [ ] Heavy logic delegated to `src/core/` and `src/utils/` (not the command class) +- [ ] Proper logging for debugging +- [ ] Graceful handling of interruptions + +## 7. Contentstack Integration Review + +### API Usage +- [ ] Proper authentication using CLI utilities +- [ ] Rate limiting respected (10 req/sec for Management API) +- [ ] Appropriate error handling for API-specific errors +- [ ] Retry logic for transient failures + +### Query export behavior +- [ ] Query parsing and flags behave as documented +- [ ] Dependency / reference / asset handling respects `skip-*` flags +- [ ] Failures surface clearly to the user (no silent drops) +- [ ] Logging useful for support without leaking secrets + +### Environment Management +- [ ] Environment validation before operations +- [ ] Cross-environment operations handled safely +- [ ] Proper handling of environment-specific configurations +- [ ] Content type validation + +## Review Checklist Summary + +### Before Approving +- [ ] All critical issues resolved +- [ ] Tests pass; coverage reasonable for the change (~80% repo-wide is aspirational) +- [ ] Security concerns addressed +- [ ] Performance implications considered +- [ ] Documentation updated if needed +- [ ] Breaking changes properly communicated + +### Review Quality +- [ ] Code thoroughly examined, not just skimmed +- [ ] Constructive feedback provided +- [ ] Questions asked for unclear implementations +- [ ] Best practices enforced consistently +- [ ] Knowledge shared through comments + +### Post-Review +- [ ] Appropriate merge strategy selected +- [ ] Deployment considerations discussed +- [ ] Team notified of significant changes +- [ ] Follow-up tasks created if needed \ No newline at end of file diff --git a/skills/code-review/references/code-review-checklist.md b/skills/code-review/references/code-review-checklist.md deleted file mode 100644 index bb30248..0000000 --- a/skills/code-review/references/code-review-checklist.md +++ /dev/null @@ -1,172 +0,0 @@ -# Code Review Checklist - -Comprehensive PR review guidelines for **`@contentstack/cli-cm-export-query`** and related CLI work. - -## Review Process - -### Severity Levels -- 🔴 **Critical**: Must fix before merge (security, correctness, breaking changes) -- 🟡 **Important**: Should fix (performance, maintainability, best practices) -- 🟢 **Suggestion**: Consider improving (style, optimization, readability) - -## 1. Security Review - -### Authentication & Authorization -- [ ] No hardcoded API keys, tokens, or credentials -- [ ] Sensitive data not logged to console or files -- [ ] Proper token validation and expiration handling -- [ ] Environment variables used for secrets - -### Input Validation -- [ ] All user inputs validated and sanitized -- [ ] Command flags properly validated -- [ ] File paths sanitized to prevent directory traversal -- [ ] API responses validated before processing - -### Error Handling -- [ ] Errors don't expose sensitive information -- [ ] Stack traces filtered in production -- [ ] Proper error logging without secrets - -## 2. Correctness Review - -### Logic Validation -- [ ] Business logic correctly implemented -- [ ] Edge cases handled appropriately -- [ ] Null/undefined checks in place -- [ ] Async operations properly awaited - -### Error Scenarios -- [ ] Network failures handled gracefully -- [ ] Rate limiting respected and handled -- [ ] Partial failures in batch operations managed -- [ ] Retry logic implemented correctly - -### Data Integrity -- [ ] Data transformations are reversible where needed -- [ ] Batch operations maintain consistency -- [ ] Rollback mechanisms for critical operations -- [ ] Proper validation before destructive operations - -## 3. Architecture Review - -### Code Organization -- [ ] Proper separation of concerns (Commands → Services → Utils) -- [ ] Single responsibility principle followed -- [ ] Dependencies injected, not hardcoded -- [ ] Interfaces used for abstractions - -### Design Patterns -- [ ] Consistent error handling patterns -- [ ] Proper use of async/await -- [ ] Service layer properly abstracted -- [ ] Configuration management centralized - -### Modularity -- [ ] Functions are focused and testable -- [ ] Classes have clear responsibilities -- [ ] Modules are loosely coupled -- [ ] Common functionality extracted to utilities - -## 4. Performance Review - -### Efficiency -- [ ] Unnecessary API calls eliminated -- [ ] Export/query work batched or paginated where the Management API requires it -- [ ] Proper pagination implemented when listing large result sets -- [ ] Rate limiting respected - -### Resource Management -- [ ] Memory usage optimized for large datasets -- [ ] File handles properly closed -- [ ] Network connections cleaned up -- [ ] No memory leaks in long-running operations - -### Concurrency -- [ ] Appropriate concurrency limits set -- [ ] Race conditions avoided -- [ ] Deadlocks prevented -- [ ] Resource contention minimized - -## 5. Testing Review - -### Test Coverage -- [ ] All new/modified code has tests -- [ ] Both success and failure paths tested -- [ ] Edge cases covered -- [ ] Integration tests for complex workflows - -### Test Quality -- [ ] Tests are focused and readable -- [ ] Proper mocking of external dependencies -- [ ] No real API calls in tests -- [ ] Test data is realistic and maintainable - -### TDD / test discipline -- [ ] New behavior covered by tests where practical (test-first preferred, not mandatory for refactors/docs) -- [ ] Tests fail appropriately when code is broken -- [ ] Tests are independent and can run in any order -- [ ] No test.skip or .only in committed code - -## 6. CLI-Specific Review - -### OCLIF Command Structure -- [ ] Extends appropriate base command class -- [ ] Proper flag definitions with validation -- [ ] Clear command description and examples -- [ ] Appropriate error handling with user-friendly messages - -### User Experience -- [ ] Progress indicators for long operations -- [ ] Clear success/failure messaging -- [ ] Proper use of colors and formatting -- [ ] Confirmation prompts for destructive actions - -### Command patterns -- [ ] Input validation before processing -- [ ] Heavy logic delegated to `src/core/` and `src/utils/` (not the command class) -- [ ] Proper logging for debugging -- [ ] Graceful handling of interruptions - -## 7. Contentstack Integration Review - -### API Usage -- [ ] Proper authentication using CLI utilities -- [ ] Rate limiting respected (10 req/sec for Management API) -- [ ] Appropriate error handling for API-specific errors -- [ ] Retry logic for transient failures - -### Query export behavior -- [ ] Query parsing and flags behave as documented -- [ ] Dependency / reference / asset handling respects `skip-*` flags -- [ ] Failures surface clearly to the user (no silent drops) -- [ ] Logging useful for support without leaking secrets - -### Environment Management -- [ ] Environment validation before operations -- [ ] Cross-environment operations handled safely -- [ ] Proper handling of environment-specific configurations -- [ ] Content type validation - -## Review Checklist Summary - -### Before Approving -- [ ] All critical issues resolved -- [ ] Tests pass; coverage reasonable for the change (~80% repo-wide is aspirational) -- [ ] Security concerns addressed -- [ ] Performance implications considered -- [ ] Documentation updated if needed -- [ ] Breaking changes properly communicated - -### Review Quality -- [ ] Code thoroughly examined, not just skimmed -- [ ] Constructive feedback provided -- [ ] Questions asked for unclear implementations -- [ ] Best practices enforced consistently -- [ ] Knowledge shared through comments - -### Post-Review -- [ ] Appropriate merge strategy selected -- [ ] Deployment considerations discussed -- [ ] Team notified of significant changes -- [ ] Follow-up tasks created if needed \ No newline at end of file diff --git a/skills/contentstack-cli/SKILL.md b/skills/contentstack-cli/SKILL.md index 33442d9..f78d3ee 100644 --- a/skills/contentstack-cli/SKILL.md +++ b/skills/contentstack-cli/SKILL.md @@ -5,10 +5,7 @@ description: Contentstack CLI query-export plugin — OCLIF command, QueryExport # Contentstack CLI — query export -## Quick reference - -- **[Contentstack patterns](references/contentstack-patterns.md)** — Command shape, export pipeline, API habits -- **[Framework patterns](../framework/references/framework-patterns.md)** — Config, logging, errors +Guidance for **`@contentstack/cli-cm-export-query`**: query-driven export with dependency and asset handling. ## This package @@ -24,6 +21,101 @@ description: Contentstack CLI query-export plugin — OCLIF command, QueryExport - Respect rate limits and handle **429** / transient errors when adding API calls. - Tests: mock SDK and file I/O; no real stack access in unit tests. -## Usage +## Repository layout + +| Area | Role | +|------|------| +| `src/commands/cm/stacks/export-query.ts` | CLI entry: flags, config setup, `QueryExporter` | +| `src/core/query-executor.ts` | `QueryExporter` — main export pipeline | +| `src/core/module-exporter.ts` | `ModuleExporter` — module export details | +| `src/utils/` | Query parser, config, branches, dependencies, assets, files, logger | +| `src/types/index.ts` | Shared types (e.g. `QueryExportConfig`, `Modules`) | +| `src/config/` | Defaults (copied to `lib/` on build) | + +There is **no** `src/services/` directory in this repo. + +## Command pattern + +Use **`@contentstack/cli-command`** and **`@contentstack/cli-utilities`**: + +```typescript +import { Command } from '@contentstack/cli-command'; +import { + flags, + FlagInput, + managementSDKClient, + log, + handleAndLogError, +} from '@contentstack/cli-utilities'; +import { QueryExporter } from '../../../core/query-executor'; + +export default class ExportQueryCommand extends Command { + static description = 'Export content from a stack using query-based filtering'; + + static flags: FlagInput = { + query: flags.string({ + required: true, + description: 'Query as JSON string or file path', + }), + alias: flags.string({ char: 'a', description: 'Management token alias' }), + // ...see export-query.ts for full flags + }; + + async run(): Promise { + const { flags } = await this.parse(ExportQueryCommand); + // setupQueryExportConfig(flags), managementSDKClient(...), then: + // const exporter = new QueryExporter(client, exportQueryConfig); + // await exporter.execute(); + } +} +``` + +### Conventions + +- Validate required inputs early (`query`, stack credentials). +- Use **`log`** with export **context** objects for structured messages. +- Use **`handleAndLogError`** for consistent error reporting where the codebase already does. + +## Export pipeline (conceptual) + +1. **Parse** query via **`QueryParser`** (JSON string or path to JSON file). +2. **Export** general and queried modules (aligned with **`@contentstack/cli-cm-export`**). +3. **Resolve** content types, references, and assets unless disabled (`skip-references`, `skip-dependencies`, `secured-assets`). + +When extending behavior, prefer new methods on **`QueryExporter`** / **`ModuleExporter`** or focused utils under **`src/utils/`**. + +## Authentication and secrets + +- Resolve tokens through CLI utilities and command flags; do not print management tokens or API keys. +- Do not write secrets into export directories. + +## API and rate limits + +- Contentstack APIs are rate-limited; use delays or backoff on **429** when introducing new call patterns. +- In tests, stub **`managementSDKClient`**, stack client methods, and **`fsUtil`** as the existing unit tests do. + +--- + +## Other CLI plugins (context) + +Other Contentstack CLI packages sometimes use **`BaseBulkCommand`**, batch processors, or JSON logs under `bulk-operation/`. **This query-export plugin does not use those patterns.** + +### Rate limit sketch (generic) + +```typescript +class RateLimiter { + private lastRequest = 0; + private readonly minIntervalMs = 100; // order-of-magnitude; tune per API guidance + + async wait(): Promise { + const now = Date.now(); + const elapsed = now - this.lastRequest; + if (elapsed < this.minIntervalMs) { + await new Promise((r) => setTimeout(r, this.minIntervalMs - elapsed)); + } + this.lastRequest = Date.now(); + } +} +``` -Use this skill when changing export behavior, flags, query handling, or Contentstack API usage. Open **references/contentstack-patterns.md** for longer examples. +Adapt to whatever **`@contentstack/cli-utilities`** or **`@contentstack/cli-cm-export`** already provides before adding parallel limiters. diff --git a/skills/contentstack-cli/references/contentstack-patterns.md b/skills/contentstack-cli/references/contentstack-patterns.md deleted file mode 100644 index e65b6ec..0000000 --- a/skills/contentstack-cli/references/contentstack-patterns.md +++ /dev/null @@ -1,102 +0,0 @@ -# Contentstack patterns — query export plugin - -Guidance for **`@contentstack/cli-cm-export-query`**: query-driven export with dependency and asset handling. - -## Repository layout - -| Area | Role | -|------|------| -| `src/commands/cm/stacks/export-query.ts` | CLI entry: flags, config setup, `QueryExporter` | -| `src/core/query-executor.ts` | `QueryExporter` — main export pipeline | -| `src/core/module-exporter.ts` | `ModuleExporter` — module export details | -| `src/utils/` | Query parser, config, branches, dependencies, assets, files, logger | -| `src/types/index.ts` | Shared types (e.g. `QueryExportConfig`, `Modules`) | -| `src/config/` | Defaults (copied to `lib/` on build) | - -There is **no** `src/services/` directory in this repo. - -## Command pattern - -Use **`@contentstack/cli-command`** and **`@contentstack/cli-utilities`**: - -```typescript -import { Command } from '@contentstack/cli-command'; -import { - flags, - FlagInput, - managementSDKClient, - log, - handleAndLogError, -} from '@contentstack/cli-utilities'; -import { QueryExporter } from '../../../core/query-executor'; - -export default class ExportQueryCommand extends Command { - static description = 'Export content from a stack using query-based filtering'; - - static flags: FlagInput = { - query: flags.string({ - required: true, - description: 'Query as JSON string or file path', - }), - alias: flags.string({ char: 'a', description: 'Management token alias' }), - // ...see export-query.ts for full flags - }; - - async run(): Promise { - const { flags } = await this.parse(ExportQueryCommand); - // setupQueryExportConfig(flags), managementSDKClient(...), then: - // const exporter = new QueryExporter(client, exportQueryConfig); - // await exporter.execute(); - } -} -``` - -### Conventions - -- Validate required inputs early (`query`, stack credentials). -- Use **`log`** with export **context** objects for structured messages. -- Use **`handleAndLogError`** for consistent error reporting where the codebase already does. - -## Export pipeline (conceptual) - -1. **Parse** query via **`QueryParser`** (JSON string or path to JSON file). -2. **Export** general and queried modules (aligned with **`@contentstack/cli-cm-export`**). -3. **Resolve** content types, references, and assets unless disabled (`skip-references`, `skip-dependencies`, `secured-assets`). - -When extending behavior, prefer new methods on **`QueryExporter`** / **`ModuleExporter`** or focused utils under **`src/utils/`**. - -## Authentication and secrets - -- Resolve tokens through CLI utilities and command flags; do not print management tokens or API keys. -- Do not write secrets into export directories. - -## API and rate limits - -- Contentstack APIs are rate-limited; use delays or backoff on **429** when introducing new call patterns. -- In tests, stub **`managementSDKClient`**, stack client methods, and **`fsUtil`** as the existing unit tests do. - ---- - -## Other CLI plugins (context) - -Other Contentstack CLI packages sometimes use **`BaseBulkCommand`**, batch processors, or JSON logs under `bulk-operation/`. **This query-export plugin does not use those patterns.** - -### Rate limit sketch (generic) - -```typescript -class RateLimiter { - private lastRequest = 0; - private readonly minIntervalMs = 100; // order-of-magnitude; tune per API guidance - - async wait(): Promise { - const now = Date.now(); - const elapsed = now - this.lastRequest; - if (elapsed < this.minIntervalMs) { - await new Promise((r) => setTimeout(r, this.minIntervalMs - elapsed)); - } - this.lastRequest = Date.now(); - } -} -``` - -Adapt to whatever **`@contentstack/cli-utilities`** or **`@contentstack/cli-cm-export`** already provides before adding parallel limiters. diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md new file mode 100644 index 0000000..2845935 --- /dev/null +++ b/skills/dev-workflow/SKILL.md @@ -0,0 +1,35 @@ +--- +name: dev-workflow +description: CI, Husky hooks, branch and PR expectations for the cli-cm-export-query plugin repo. +--- + +# Development workflow – CLI export-query + +## When to use + +- Running builds/tests before a PR +- Understanding which GitHub Actions run on this package +- Husky / pre-commit expectations + +## Commands + +| Command | Purpose | +| --- | --- | +| `npm run build` | Clean, install, compile, copy `src/config` → `lib/` | +| `npm test` | `pretest` compiles tests; nyc + mocha `test/**/*.test.ts` | +| `npm run test:unit` | Mocha `test/unit/**/*.test.ts` only | +| `npm run lint` | ESLint `src/**/*.ts` | +| `npm run prepack` | Compile + OCLIF manifest/readme + config copy (release path) | + +## CI + +Workflows under [`.github/workflows/`](../../../.github/workflows/): e.g. `unit-test.yml`, `release.yml`, `sca-scan.yml`, `policy-scan.yml`. + +## Git hooks + +- `prepare` runs Husky setup (see `package.json`); hooks live under [`.husky/`](../../../.husky/) when configured. + +## PR expectations + +- Tests and lint pass; no `describe.only` / `it.only` (`--forbid-only` in test scripts). +- Coordinate with [testing](../testing/SKILL.md) and [code-review](../code-review/SKILL.md). diff --git a/skills/framework/SKILL.md b/skills/framework/SKILL.md index c6799aa..6c75b92 100644 --- a/skills/framework/SKILL.md +++ b/skills/framework/SKILL.md @@ -3,70 +3,193 @@ name: framework description: Utilities, configuration, logging, and error patterns for @contentstack/cli-cm-export-query. Use when working in src/utils/, config, or shared helpers — align with @contentstack/cli-utilities where possible. --- -# Framework Patterns Skill +# Framework Patterns -## Quick Reference +Core utilities, configuration, logging, and error-handling patterns for **`@contentstack/cli-cm-export-query`** (and similar CLI plugins). Prefer matching patterns already in **`src/utils/`** and **`@contentstack/cli-utilities`** before introducing new abstractions. -For comprehensive framework guidance, see: -- **[Framework Patterns](references/framework-patterns.md)** - Complete utilities, configuration, logging, and framework patterns +## Configuration Management -## Core Framework Components +```typescript +export interface AppConfig { + contentstack: { apiKey: string; authToken: string; region: string; }; + batch: { defaultSize: number; maxConcurrency: number; retryAttempts: number; }; + logging: { level: string; format: string; }; +} -### Configuration Management -- Centralized configuration with environment-specific overrides -- Validation of required configuration values -- Type-safe configuration interfaces +export class ConfigBuilder { + static build(): AppConfig { + return { + contentstack: { apiKey: process.env.CONTENTSTACK_API_KEY!, authToken: process.env.CONTENTSTACK_AUTH_TOKEN!, region: process.env.CONTENTSTACK_REGION || 'us' }, + batch: { defaultSize: parseInt(process.env.BATCH_SIZE || '10'), maxConcurrency: parseInt(process.env.MAX_CONCURRENCY || '3'), retryAttempts: parseInt(process.env.RETRY_ATTEMPTS || '3') }, + logging: { level: process.env.LOG_LEVEL || 'info', format: process.env.LOG_FORMAT || 'json' } + }; + } + static validate(config: AppConfig): void { + if (!config.contentstack.apiKey) throw new Error('CONTENTSTACK_API_KEY is required'); + if (!config.contentstack.authToken) throw new Error('CONTENTSTACK_AUTH_TOKEN is required'); + } +} +``` -### Logging Framework -- Structured logging with different levels (debug, info, warn, error) -- Consistent log formatting and metadata -- Context-aware logging for debugging +## Logging Framework -### Error Handling Framework -- Consistent error handling with context and categorization -- Custom error classes for different error types -- Proper error propagation and logging +```typescript +export interface Logger { debug(message: string, meta?: object): void; info(message: string, meta?: object): void; warn(message: string, meta?: object): void; error(message: string, meta?: object): void; } -### Utility Classes -- Rate limiter for API throttling -- Retry strategy with exponential backoff -- Batch or chunked processing when exporting large module sets (if introduced) -- File system utilities with error handling +export class ConsoleLogger implements Logger { + constructor(private level: string = 'info') {} + debug(message: string, meta?: object): void { if (this.shouldLog('debug')) console.debug(this.format('DEBUG', message, meta)); } + info(message: string, meta?: object): void { if (this.shouldLog('info')) console.info(this.format('INFO', message, meta)); } + warn(message: string, meta?: object): void { if (this.shouldLog('warn')) console.warn(this.format('WARN', message, meta)); } + error(message: string, meta?: object): void { if (this.shouldLog('error')) console.error(this.format('ERROR', message, meta)); } -## Quick Patterns + private shouldLog(level: string): boolean { const levels = ['debug', 'info', 'warn', 'error']; return levels.indexOf(level) >= levels.indexOf(this.level); } + private format(level: string, message: string, meta?: object): string { return JSON.stringify({ timestamp: new Date().toISOString(), level, message, ...meta }); } +} +``` + +## Error Handling Framework -### Configuration Builder ```typescript -export class ConfigBuilder { - static build(): AppConfig { - return { - contentstack: { - apiKey: process.env.CONTENTSTACK_API_KEY!, - authToken: process.env.CONTENTSTACK_AUTH_TOKEN! - }, - batch: { - defaultSize: parseInt(process.env.BATCH_SIZE || '10'), - maxConcurrency: parseInt(process.env.MAX_CONCURRENCY || '3') +export abstract class BaseError extends Error { + abstract readonly code: string; abstract readonly category: 'validation' | 'api' | 'system' | 'user'; + constructor(message: string, public readonly context?: Record, public readonly cause?: Error) { super(message); this.name = this.constructor.name; } +} + +export class ValidationError extends BaseError { readonly code = 'VALIDATION_ERROR'; readonly category = 'validation' as const; } + +export class ApiError extends BaseError { + readonly code = 'API_ERROR'; readonly category = 'api' as const; + constructor(message: string, public readonly status?: number, context?: Record, cause?: Error) { super(message, { ...context, status }, cause); } +} + +export class ContentstackApiError extends ApiError { + readonly code = 'CONTENTSTACK_API_ERROR'; + static fromResponse(response: any, context?: Record): ContentstackApiError { + return new ContentstackApiError(response.error_message || 'API request failed', response.error_code, { ...context, errorCode: response.error_code, details: response.errors }); + } +} +``` + +## Utility Classes + +### Rate Limiter +```typescript +export class RateLimiter { + private queue: Array<() => void> = []; private running = 0; private lastRequest = 0; + constructor(private maxConcurrent: number = 1, private minInterval: number = 100) {} + + async execute(operation: () => Promise): Promise { + return new Promise((resolve, reject) => { + this.queue.push(async () => { + try { await this.waitForInterval(); this.running++; const result = await operation(); resolve(result); } + catch (error) { reject(error); } finally { this.running--; this.processQueue(); } + }); + this.processQueue(); + }); + } + + private processQueue(): void { if (this.running < this.maxConcurrent && this.queue.length > 0) this.queue.shift()!(); } + private async waitForInterval(): Promise { + const now = Date.now(); const elapsed = now - this.lastRequest; + if (elapsed < this.minInterval) await new Promise(resolve => setTimeout(resolve, this.minInterval - elapsed)); + this.lastRequest = Date.now(); + } +} +``` + +### Retry Strategy +```typescript +export interface RetryOptions { maxAttempts: number; initialDelay: number; maxDelay: number; backoffFactor: number; retryCondition?: (error: any) => boolean; } + +export class RetryStrategy { + constructor(private options: RetryOptions) {} + async execute(operation: () => Promise): Promise { + let lastError: any; let delay = this.options.initialDelay; + for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) { + try { return await operation(); } + catch (error) { + lastError = error; if (this.options.retryCondition && !this.options.retryCondition(error)) throw error; + if (attempt === this.options.maxAttempts) break; await new Promise(resolve => setTimeout(resolve, delay)); + delay = Math.min(delay * this.options.backoffFactor, this.options.maxDelay); } - }; + } + throw lastError; + } + + static forContentstack(): RetryStrategy { + return new RetryStrategy({ maxAttempts: 3, initialDelay: 1000, maxDelay: 10000, backoffFactor: 2, retryCondition: (error) => error.status === 429 || (error.status >= 500 && error.status < 600) }); } } ``` -### Error Handling +### Batch Processor ```typescript -export class ValidationError extends BaseError { - readonly code = 'VALIDATION_ERROR'; - readonly category = 'validation' as const; +export interface BatchOptions { batchSize: number; concurrency: number; processor: (item: T) => Promise; onProgress?: (completed: number, total: number) => void; } + +export class BatchProcessor { + static async process(items: T[], options: BatchOptions): Promise { + const batches = this.chunk(items, options.batchSize); const allResults: any[] = []; let completed = 0; + const processBatch = async (batch: T[]): Promise => { + const results = await Promise.allSettled(batch.map(options.processor)); allResults.push(...results); completed += batch.length; options.onProgress?.(completed, items.length); + }; + const semaphore = new Semaphore(options.concurrency); await Promise.all(batches.map(batch => semaphore.acquire(() => processBatch(batch)))); return allResults; + } + private static chunk(array: T[], size: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < array.length; i += size) chunks.push(array.slice(i, i + size)); return chunks; } +} + +class Semaphore { + private permits: number; private waiting: Array<() => void> = []; + constructor(permits: number) { this.permits = permits; } + async acquire(task: () => Promise): Promise { + return new Promise((resolve, reject) => { + const tryAcquire = () => { + if (this.permits > 0) { this.permits--; task().then(resolve).catch(reject).finally(() => { this.permits++; if (this.waiting.length > 0) this.waiting.shift()!(); }); } + else this.waiting.push(tryAcquire); + }; + tryAcquire(); + }); + } } ``` -### Rate Limiter +## File System & Validation Utilities + ```typescript -const rateLimiter = new RateLimiter(3, 100); // 3 concurrent, 100ms interval -await rateLimiter.execute(() => apiCall()); +export class FileUtil { + static async writeJson(filePath: string, data: any): Promise { + try { const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(filePath, JSON.stringify(data, null, 2)); } + catch (error) { throw new Error(`Failed to write file ${filePath}: ${error.message}`); } + } + static async readJson(filePath: string): Promise { + try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content); } + catch (error) { if (error.code === 'ENOENT') throw new Error(`File not found: ${filePath}`); throw new Error(`Failed to read file ${filePath}: ${error.message}`); } + } + static async exists(filePath: string): Promise { try { await fs.access(filePath); return true; } catch { return false; } } +} + +export class Validator { + static required(value: any, fieldName: string): void { if (value === null || value === undefined || value === '') throw new ValidationError(`${fieldName} is required`); } + static isArray(value: any, fieldName: string): void { if (!Array.isArray(value)) throw new ValidationError(`${fieldName} must be an array`); } + static isString(value: any, fieldName: string): void { if (typeof value !== 'string') throw new ValidationError(`${fieldName} must be a string`); } + static validateEnvironment(env: string): void { this.required(env, 'environment'); this.isString(env, 'environment'); } + static validateBatchSize(size: number): void { this.required(size, 'batchSize'); if (size < 1 || size > 100) throw new ValidationError('batchSize must be between 1 and 100'); } +} ``` -## Usage +## Dependency Injection -This skill provides Cursor-specific framework integration. Reference the universal framework patterns above for detailed implementations of configuration, logging, error handling, utilities, and dependency injection patterns that work with any AI agent. \ No newline at end of file +```typescript +export class Container { + private services = new Map(); private factories = new Map any>(); + register(name: string, factory: () => T): void { this.factories.set(name, factory); } + get(name: string): T { + if (this.services.has(name)) return this.services.get(name); const factory = this.factories.get(name); if (!factory) throw new Error(`Service not registered: ${name}`); + const instance = factory(); this.services.set(name, instance); return instance; + } + static setup(): Container { + const container = new Container(); container.register('config', () => ConfigBuilder.build()); container.register('logger', () => new ConsoleLogger()); + container.register('rateLimiter', () => new RateLimiter(3, 100)); container.register('retryStrategy', () => RetryStrategy.forContentstack()); return container; + } +} +``` \ No newline at end of file diff --git a/skills/framework/references/framework-patterns.md b/skills/framework/references/framework-patterns.md deleted file mode 100644 index e3131e2..0000000 --- a/skills/framework/references/framework-patterns.md +++ /dev/null @@ -1,190 +0,0 @@ -# Framework Patterns - -Core utilities, configuration, logging, and error-handling patterns for **`@contentstack/cli-cm-export-query`** (and similar CLI plugins). Prefer matching patterns already in **`src/utils/`** and **`@contentstack/cli-utilities`** before introducing new abstractions. - -## Configuration Management - -```typescript -export interface AppConfig { - contentstack: { apiKey: string; authToken: string; region: string; }; - batch: { defaultSize: number; maxConcurrency: number; retryAttempts: number; }; - logging: { level: string; format: string; }; -} - -export class ConfigBuilder { - static build(): AppConfig { - return { - contentstack: { apiKey: process.env.CONTENTSTACK_API_KEY!, authToken: process.env.CONTENTSTACK_AUTH_TOKEN!, region: process.env.CONTENTSTACK_REGION || 'us' }, - batch: { defaultSize: parseInt(process.env.BATCH_SIZE || '10'), maxConcurrency: parseInt(process.env.MAX_CONCURRENCY || '3'), retryAttempts: parseInt(process.env.RETRY_ATTEMPTS || '3') }, - logging: { level: process.env.LOG_LEVEL || 'info', format: process.env.LOG_FORMAT || 'json' } - }; - } - static validate(config: AppConfig): void { - if (!config.contentstack.apiKey) throw new Error('CONTENTSTACK_API_KEY is required'); - if (!config.contentstack.authToken) throw new Error('CONTENTSTACK_AUTH_TOKEN is required'); - } -} -``` - -## Logging Framework - -```typescript -export interface Logger { debug(message: string, meta?: object): void; info(message: string, meta?: object): void; warn(message: string, meta?: object): void; error(message: string, meta?: object): void; } - -export class ConsoleLogger implements Logger { - constructor(private level: string = 'info') {} - debug(message: string, meta?: object): void { if (this.shouldLog('debug')) console.debug(this.format('DEBUG', message, meta)); } - info(message: string, meta?: object): void { if (this.shouldLog('info')) console.info(this.format('INFO', message, meta)); } - warn(message: string, meta?: object): void { if (this.shouldLog('warn')) console.warn(this.format('WARN', message, meta)); } - error(message: string, meta?: object): void { if (this.shouldLog('error')) console.error(this.format('ERROR', message, meta)); } - - private shouldLog(level: string): boolean { const levels = ['debug', 'info', 'warn', 'error']; return levels.indexOf(level) >= levels.indexOf(this.level); } - private format(level: string, message: string, meta?: object): string { return JSON.stringify({ timestamp: new Date().toISOString(), level, message, ...meta }); } -} -``` - -## Error Handling Framework - -```typescript -export abstract class BaseError extends Error { - abstract readonly code: string; abstract readonly category: 'validation' | 'api' | 'system' | 'user'; - constructor(message: string, public readonly context?: Record, public readonly cause?: Error) { super(message); this.name = this.constructor.name; } -} - -export class ValidationError extends BaseError { readonly code = 'VALIDATION_ERROR'; readonly category = 'validation' as const; } - -export class ApiError extends BaseError { - readonly code = 'API_ERROR'; readonly category = 'api' as const; - constructor(message: string, public readonly status?: number, context?: Record, cause?: Error) { super(message, { ...context, status }, cause); } -} - -export class ContentstackApiError extends ApiError { - readonly code = 'CONTENTSTACK_API_ERROR'; - static fromResponse(response: any, context?: Record): ContentstackApiError { - return new ContentstackApiError(response.error_message || 'API request failed', response.error_code, { ...context, errorCode: response.error_code, details: response.errors }); - } -} -``` - -## Utility Classes - -### Rate Limiter -```typescript -export class RateLimiter { - private queue: Array<() => void> = []; private running = 0; private lastRequest = 0; - constructor(private maxConcurrent: number = 1, private minInterval: number = 100) {} - - async execute(operation: () => Promise): Promise { - return new Promise((resolve, reject) => { - this.queue.push(async () => { - try { await this.waitForInterval(); this.running++; const result = await operation(); resolve(result); } - catch (error) { reject(error); } finally { this.running--; this.processQueue(); } - }); - this.processQueue(); - }); - } - - private processQueue(): void { if (this.running < this.maxConcurrent && this.queue.length > 0) this.queue.shift()!(); } - private async waitForInterval(): Promise { - const now = Date.now(); const elapsed = now - this.lastRequest; - if (elapsed < this.minInterval) await new Promise(resolve => setTimeout(resolve, this.minInterval - elapsed)); - this.lastRequest = Date.now(); - } -} -``` - -### Retry Strategy -```typescript -export interface RetryOptions { maxAttempts: number; initialDelay: number; maxDelay: number; backoffFactor: number; retryCondition?: (error: any) => boolean; } - -export class RetryStrategy { - constructor(private options: RetryOptions) {} - async execute(operation: () => Promise): Promise { - let lastError: any; let delay = this.options.initialDelay; - for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) { - try { return await operation(); } - catch (error) { - lastError = error; if (this.options.retryCondition && !this.options.retryCondition(error)) throw error; - if (attempt === this.options.maxAttempts) break; await new Promise(resolve => setTimeout(resolve, delay)); - delay = Math.min(delay * this.options.backoffFactor, this.options.maxDelay); - } - } - throw lastError; - } - - static forContentstack(): RetryStrategy { - return new RetryStrategy({ maxAttempts: 3, initialDelay: 1000, maxDelay: 10000, backoffFactor: 2, retryCondition: (error) => error.status === 429 || (error.status >= 500 && error.status < 600) }); - } -} -``` - -### Batch Processor -```typescript -export interface BatchOptions { batchSize: number; concurrency: number; processor: (item: T) => Promise; onProgress?: (completed: number, total: number) => void; } - -export class BatchProcessor { - static async process(items: T[], options: BatchOptions): Promise { - const batches = this.chunk(items, options.batchSize); const allResults: any[] = []; let completed = 0; - const processBatch = async (batch: T[]): Promise => { - const results = await Promise.allSettled(batch.map(options.processor)); allResults.push(...results); completed += batch.length; options.onProgress?.(completed, items.length); - }; - const semaphore = new Semaphore(options.concurrency); await Promise.all(batches.map(batch => semaphore.acquire(() => processBatch(batch)))); return allResults; - } - private static chunk(array: T[], size: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < array.length; i += size) chunks.push(array.slice(i, i + size)); return chunks; } -} - -class Semaphore { - private permits: number; private waiting: Array<() => void> = []; - constructor(permits: number) { this.permits = permits; } - async acquire(task: () => Promise): Promise { - return new Promise((resolve, reject) => { - const tryAcquire = () => { - if (this.permits > 0) { this.permits--; task().then(resolve).catch(reject).finally(() => { this.permits++; if (this.waiting.length > 0) this.waiting.shift()!(); }); } - else this.waiting.push(tryAcquire); - }; - tryAcquire(); - }); - } -} -``` - -## File System & Validation Utilities - -```typescript -export class FileUtil { - static async writeJson(filePath: string, data: any): Promise { - try { const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(filePath, JSON.stringify(data, null, 2)); } - catch (error) { throw new Error(`Failed to write file ${filePath}: ${error.message}`); } - } - static async readJson(filePath: string): Promise { - try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content); } - catch (error) { if (error.code === 'ENOENT') throw new Error(`File not found: ${filePath}`); throw new Error(`Failed to read file ${filePath}: ${error.message}`); } - } - static async exists(filePath: string): Promise { try { await fs.access(filePath); return true; } catch { return false; } } -} - -export class Validator { - static required(value: any, fieldName: string): void { if (value === null || value === undefined || value === '') throw new ValidationError(`${fieldName} is required`); } - static isArray(value: any, fieldName: string): void { if (!Array.isArray(value)) throw new ValidationError(`${fieldName} must be an array`); } - static isString(value: any, fieldName: string): void { if (typeof value !== 'string') throw new ValidationError(`${fieldName} must be a string`); } - static validateEnvironment(env: string): void { this.required(env, 'environment'); this.isString(env, 'environment'); } - static validateBatchSize(size: number): void { this.required(size, 'batchSize'); if (size < 1 || size > 100) throw new ValidationError('batchSize must be between 1 and 100'); } -} -``` - -## Dependency Injection - -```typescript -export class Container { - private services = new Map(); private factories = new Map any>(); - register(name: string, factory: () => T): void { this.factories.set(name, factory); } - get(name: string): T { - if (this.services.has(name)) return this.services.get(name); const factory = this.factories.get(name); if (!factory) throw new Error(`Service not registered: ${name}`); - const instance = factory(); this.services.set(name, instance); return instance; - } - static setup(): Container { - const container = new Container(); container.register('config', () => ConfigBuilder.build()); container.register('logger', () => new ConsoleLogger()); - container.register('rateLimiter', () => new RateLimiter(3, 100)); container.register('retryStrategy', () => RetryStrategy.forContentstack()); return container; - } -} -``` \ No newline at end of file diff --git a/skills/testing/SKILL.md b/skills/testing/SKILL.md index b7b859a..35b660a 100644 --- a/skills/testing/SKILL.md +++ b/skills/testing/SKILL.md @@ -5,30 +5,17 @@ description: Mocha/Chai/Sinon testing and TDD for @contentstack/cli-cm-export-qu # Testing Patterns -## Quick Reference - -For comprehensive testing guidance, see: -- **[Testing Patterns](./references/testing-patterns.md)** - Complete testing best practices and TDD workflow -- **[Development Workflow](./references/development-workflow.md)** - TDD process and validation requirements - -## TDD workflow summary +Testing best practices and TDD workflow for **`@contentstack/cli-cm-export-query`**. **RED → GREEN → REFACTOR** for behavior changes; pure refactors / docs-only may skip new tests when behavior is unchanged. -## Key testing rules - -- **~80% coverage** (lines, branches, functions) is **aspirational**, not a hard CI gate here -- **Use sinon** for API responses and mocking -- **Never make real API calls** in tests -- **Mock at service boundaries**, not implementation details -- **Test both success and failure paths** -- **Use descriptive test names**: "should [behavior] when [condition]" - -## Quick Test Template +## Test Structure Standards +### Basic Test Template ```typescript describe('[ComponentName]', () => { beforeEach(() => { + // Setup mocks and test data sinon.stub(ExternalService.prototype, 'method').resolves(mockData); }); @@ -37,24 +24,266 @@ describe('[ComponentName]', () => { }); it('should [expected behavior] when [condition]', () => { - // Arrange, Act, Assert + // Arrange + const input = { /* test data */ }; + + // Act + const result = component.method(input); + + // Assert + expect(result).to.equal(expectedOutput); }); }); ``` +### Command testing example +```typescript +describe('ExportQueryCommand', () => { + beforeEach(() => { + sinon.stub(ContentstackClient.prototype, 'stack').returns(mockStack); + }); + + it('should run export when query and auth are valid', async () => { + // Stub parse, setupQueryExportConfig, QueryExporter.prototype.execute, etc. + }); +}); +``` + +## Key Testing Rules + +### Coverage +- **~80%** (lines, branches, functions) is **aspirational**, not a hard gate +- Test both success and failure paths +- Include edge cases and error scenarios + +### Mocking Standards +- **Use sinon** for API responses and external dependencies +- **Never make real API calls** in tests +- **Mock at module boundaries** (SDK, `fsUtil`), not irrelevant internals +- Restore mocks in `afterEach()` to prevent test pollution + +### Test Patterns +- Use descriptive test names: "should [behavior] when [condition]" +- Keep test setup minimal and focused +- Prefer synchronous patterns when possible +- Group related tests in `describe` blocks + ## Common Mock Patterns +### API Mocking ```typescript // Mock Contentstack API sinon.stub(ContentstackClient.prototype, 'fetch').resolves(mockData); +// Mock with specific responses +sinon.stub(client, 'getEntry') + .withArgs('entry1').resolves(mockEntry1) + .withArgs('entry2').resolves(mockEntry2); +``` + +### Service Mocking +```typescript // Mock rate limiter sinon.stub(RateLimiter.prototype, 'wait').resolves(); // Mock file operations sinon.stub(fsUtil, 'writeFile').returns(true); +sinon.stub(fsUtil, 'readFile').resolves(JSON.stringify(mockData)); +``` + +### Error Simulation +```typescript +// Mock API errors +const apiError = new Error('API Error'); +apiError.status = 500; +sinon.stub(client, 'fetch').rejects(apiError); + +// Mock rate limiting +const rateLimitError = new Error('Rate limited'); +rateLimitError.status = 429; +sinon.stub(client, 'fetch').rejects(rateLimitError); +``` + +## Error Testing Patterns + +### Rate Limit Handling +```typescript +it('should handle rate limit errors', () => { + const error = new Error('Rate limited'); + error.status = 429; + + sinon.stub(client, 'fetch').rejects(error); + + expect(service.performOperation()).to.eventually.be.fulfilled; +}); +``` + +### Validation Error Testing +```typescript +it('should throw validation error for invalid input', () => { + const invalidInput = { /* invalid data */ }; + + expect(() => service.validate(invalidInput)) + .to.throw('Validation failed'); +}); +``` + +### Async Error Handling +```typescript +it('should handle async operation failures', async () => { + sinon.stub(service, 'performAsync').rejects(new Error('Operation failed')); + + try { + await service.execute(); + expect.fail('Should have thrown error'); + } catch (error) { + expect(error.message).to.include('Operation failed'); + } +}); +``` + +## Test Organization + +### File Structure +- Mirror modules under `test/unit/`: e.g. `test/unit/query-executor.test.ts`, `test/unit/query-parser-simple.test.ts` +- Use consistent naming: `[module-name].test.ts` +- Group integration tests: `test/integration/` + +### Test Data Management +- Create mock data factories: `test/fixtures/mock-factory.ts` +- Use realistic test data that matches API responses +- Share common mocks across test files + +### Test Configuration +```javascript +// .mocharc.json +{ + "require": ["ts-node/register"], + "extensions": ["ts"], + "spec": "test/**/*.test.ts", + "timeout": 5000, + "forbid-only": true +} +``` + +## Coverage and Quality + +### Coverage Enforcement +```json +// package.json nyc configuration +"nyc": { + "check-coverage": true, + "lines": 80, + "functions": 80, + "branches": 80, + "statements": 80 +} ``` -## Usage +### Quality Checklist +- [ ] All public methods tested +- [ ] Error paths covered +- [ ] Edge cases included +- [ ] Mocks properly restored +- [ ] No real API calls +- [ ] Descriptive test names +- [ ] Minimal test setup +- [ ] Fast execution (< 5s per test) + +## Development workflow + +### TDD workflow (recommended) + +For **new behavior or bug fixes**, prefer: + +1. **RED** → Failing test (or extended test) +2. **GREEN** → Minimal code to pass +3. **REFACTOR** → Improve while tests stay green + +**Exceptions:** pure refactors, documentation-only edits, and trivial non-behavior changes may skip new tests. + +## Guidelines + +- Prefer **clear tests** over async-heavy setup when you can +- **NO test.skip or .only** in commits +- **~80% coverage** (lines, branches, functions) is **aspirational**, not a CI gate +- **TypeScript** — explicit return types where practical; avoid `any` + +## File structure (this repo) + +- **Commands**: `src/commands/cm/stacks/` +- **Core**: `src/core/` (`QueryExporter`, `ModuleExporter`, …) +- **Utils**: `src/utils/` +- **Tests**: `test/unit/` — `*.test.ts` per module (e.g. `query-executor.test.ts`) + +## Naming conventions + +- **Files**: `kebab-case.ts` / `kebab-case.test.ts` +- **Classes**: `PascalCase` +- **Functions/Variables**: `camelCase` +- **Constants**: `SCREAMING_SNAKE_CASE` +- **Test descriptions**: "should [behavior] when [condition]" + +## Code quality standards + +### TypeScript +- Explicit return types for all functions +- No `any` type usage +- Strict null checks enabled +- No unused variables or imports + +### Error handling +- Use custom error classes where the codebase already does +- Include error context and cause +- Never swallow errors silently + +### Import organization +1. Node.js built-ins +2. External libraries +3. Internal modules (relative imports last) + +## Testing + +### Coverage +- Aim high; **~80%** is a guideline +- Test success and failure paths for behavior you touch +- Mock external dependencies (SDK, `fsUtil`, etc.) + +### Test structure +```typescript +describe('[ComponentName]', () => { + beforeEach(() => { + sinon.stub(ExternalService.prototype, 'method').resolves(mockData); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should [expected behavior] when [condition]', () => { + const input = { /* test data */ }; + const result = component.method(input); + expect(result).to.equal(expectedOutput); + }); +}); +``` + +### Mocking standards +- Use sinon for API response mocking +- Never make real API calls in tests +- Mock at module boundaries (SDK, `fsUtil`, etc.), not irrelevant internals + +## Commit suggestions + +- Conventional commits are optional: `feat(scope): description` +- Include tests when you change behavior +- Run lint and tests before pushing +- No debugging code (`console.log`, `debugger`) left in + +## Development process -Reference the universal testing patterns above for detailed test structures, mocking strategies, error testing patterns, and coverage requirements. This skill provides Cursor-specific integration while the universal docs work with any AI agent. \ No newline at end of file +1. **Understand** → Read relevant patterns before coding +2. **Plan** → Break down into testable units +3. **Test first** → When adding behavior, prefer failing test then implementation +4. **Validate** → `npm run lint`, `npm run test`, `npm run test:report` if you need LCOV +5. **Review** → Self-review against the code review checklist diff --git a/skills/testing/references/development-workflow.md b/skills/testing/references/development-workflow.md deleted file mode 100644 index 312dc2e..0000000 --- a/skills/testing/references/development-workflow.md +++ /dev/null @@ -1,99 +0,0 @@ -# Development Workflow - -Core development rules and Test-Driven Development (TDD) workflow for **`@contentstack/cli-cm-export-query`**. - -## TDD workflow (recommended) - -For **new behavior or bug fixes**, prefer: - -1. **RED** → Failing test (or extended test) -2. **GREEN** → Minimal code to pass -3. **REFACTOR** → Improve while tests stay green - -**Exceptions:** pure refactors, documentation-only edits, and trivial non-behavior changes may skip new tests. - -## Guidelines - -- Prefer **clear tests** over async-heavy setup when you can -- **NO test.skip or .only** in commits -- **~80% coverage** (lines, branches, functions) is **aspirational**, not a CI gate -- **TypeScript** — explicit return types where practical; avoid `any` - -## File structure (this repo) - -- **Commands**: `src/commands/cm/stacks/` -- **Core**: `src/core/` (`QueryExporter`, `ModuleExporter`, …) -- **Utils**: `src/utils/` -- **Tests**: `test/unit/` — `*.test.ts` per module (e.g. `query-executor.test.ts`) - -## Naming conventions - -- **Files**: `kebab-case.ts` / `kebab-case.test.ts` -- **Classes**: `PascalCase` -- **Functions/Variables**: `camelCase` -- **Constants**: `SCREAMING_SNAKE_CASE` -- **Test descriptions**: "should [behavior] when [condition]" - -## Code quality standards - -### TypeScript -- Explicit return types for all functions -- No `any` type usage -- Strict null checks enabled -- No unused variables or imports - -### Error handling -- Use custom error classes where the codebase already does -- Include error context and cause -- Never swallow errors silently - -### Import organization -1. Node.js built-ins -2. External libraries -3. Internal modules (relative imports last) - -## Testing - -### Coverage -- Aim high; **~80%** is a guideline -- Test success and failure paths for behavior you touch -- Mock external dependencies (SDK, `fsUtil`, etc.) - -### Test structure -```typescript -describe('[ComponentName]', () => { - beforeEach(() => { - sinon.stub(ExternalService.prototype, 'method').resolves(mockData); - }); - - afterEach(() => { - sinon.restore(); - }); - - it('should [expected behavior] when [condition]', () => { - const input = { /* test data */ }; - const result = component.method(input); - expect(result).to.equal(expectedOutput); - }); -}); -``` - -### Mocking standards -- Use sinon for API response mocking -- Never make real API calls in tests -- Mock at module boundaries (SDK, `fsUtil`, etc.), not irrelevant internals - -## Commit suggestions - -- Conventional commits are optional: `feat(scope): description` -- Include tests when you change behavior -- Run lint and tests before pushing -- No debugging code (`console.log`, `debugger`) left in - -## Development process - -1. **Understand** → Read relevant patterns before coding -2. **Plan** → Break down into testable units -3. **Test first** → When adding behavior, prefer failing test then implementation -4. **Validate** → `npm run lint`, `npm run test`, `npm run test:report` if you need LCOV -5. **Review** → Self-review against the code review checklist diff --git a/skills/testing/references/testing-patterns.md b/skills/testing/references/testing-patterns.md deleted file mode 100644 index 40c6a27..0000000 --- a/skills/testing/references/testing-patterns.md +++ /dev/null @@ -1,188 +0,0 @@ -# Testing Patterns - -Testing best practices and TDD workflow for **`@contentstack/cli-cm-export-query`**. - -## TDD workflow - -**RED → GREEN → REFACTOR** for behavior changes. Pure refactors / docs-only may skip new tests when behavior is unchanged. - -## Test Structure Standards - -### Basic Test Template -```typescript -describe('[ComponentName]', () => { - beforeEach(() => { - // Setup mocks and test data - sinon.stub(ExternalService.prototype, 'method').resolves(mockData); - }); - - afterEach(() => { - sinon.restore(); - }); - - it('should [expected behavior] when [condition]', () => { - // Arrange - const input = { /* test data */ }; - - // Act - const result = component.method(input); - - // Assert - expect(result).to.equal(expectedOutput); - }); -}); -``` - -### Command testing example -```typescript -describe('ExportQueryCommand', () => { - beforeEach(() => { - sinon.stub(ContentstackClient.prototype, 'stack').returns(mockStack); - }); - - it('should run export when query and auth are valid', async () => { - // Stub parse, setupQueryExportConfig, QueryExporter.prototype.execute, etc. - }); -}); -``` - -## Key Testing Rules - -### Coverage -- **~80%** (lines, branches, functions) is **aspirational**, not a hard gate -- Test both success and failure paths -- Include edge cases and error scenarios - -### Mocking Standards -- **Use sinon** for API responses and external dependencies -- **Never make real API calls** in tests -- **Mock at module boundaries** (SDK, `fsUtil`), not irrelevant internals -- Restore mocks in `afterEach()` to prevent test pollution - -### Test Patterns -- Use descriptive test names: "should [behavior] when [condition]" -- Keep test setup minimal and focused -- Prefer synchronous patterns when possible -- Group related tests in `describe` blocks - -## Common Mock Patterns - -### API Mocking -```typescript -// Mock Contentstack API -sinon.stub(ContentstackClient.prototype, 'fetch').resolves(mockData); - -// Mock with specific responses -sinon.stub(client, 'getEntry') - .withArgs('entry1').resolves(mockEntry1) - .withArgs('entry2').resolves(mockEntry2); -``` - -### Service Mocking -```typescript -// Mock rate limiter -sinon.stub(RateLimiter.prototype, 'wait').resolves(); - -// Mock file operations -sinon.stub(fsUtil, 'writeFile').returns(true); -sinon.stub(fsUtil, 'readFile').resolves(JSON.stringify(mockData)); -``` - -### Error Simulation -```typescript -// Mock API errors -const apiError = new Error('API Error'); -apiError.status = 500; -sinon.stub(client, 'fetch').rejects(apiError); - -// Mock rate limiting -const rateLimitError = new Error('Rate limited'); -rateLimitError.status = 429; -sinon.stub(client, 'fetch').rejects(rateLimitError); -``` - -## Error Testing Patterns - -### Rate Limit Handling -```typescript -it('should handle rate limit errors', () => { - const error = new Error('Rate limited'); - error.status = 429; - - sinon.stub(client, 'fetch').rejects(error); - - expect(service.performOperation()).to.eventually.be.fulfilled; -}); -``` - -### Validation Error Testing -```typescript -it('should throw validation error for invalid input', () => { - const invalidInput = { /* invalid data */ }; - - expect(() => service.validate(invalidInput)) - .to.throw('Validation failed'); -}); -``` - -### Async Error Handling -```typescript -it('should handle async operation failures', async () => { - sinon.stub(service, 'performAsync').rejects(new Error('Operation failed')); - - try { - await service.execute(); - expect.fail('Should have thrown error'); - } catch (error) { - expect(error.message).to.include('Operation failed'); - } -}); -``` - -## Test Organization - -### File Structure -- Mirror modules under `test/unit/`: e.g. `test/unit/query-executor.test.ts`, `test/unit/query-parser-simple.test.ts` -- Use consistent naming: `[module-name].test.ts` -- Group integration tests: `test/integration/` - -### Test Data Management -- Create mock data factories: `test/fixtures/mock-factory.ts` -- Use realistic test data that matches API responses -- Share common mocks across test files - -### Test Configuration -```javascript -// .mocharc.json -{ - "require": ["ts-node/register"], - "extensions": ["ts"], - "spec": "test/**/*.test.ts", - "timeout": 5000, - "forbid-only": true -} -``` - -## Coverage and Quality - -### Coverage Enforcement -```json -// package.json nyc configuration -"nyc": { - "check-coverage": true, - "lines": 80, - "functions": 80, - "branches": 80, - "statements": 80 -} -``` - -### Quality Checklist -- [ ] All public methods tested -- [ ] Error paths covered -- [ ] Edge cases included -- [ ] Mocks properly restored -- [ ] No real API calls -- [ ] Descriptive test names -- [ ] Minimal test setup -- [ ] Fast execution (< 5s per test) \ No newline at end of file