Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "bupkis",
|
||||
"description": "Idiomatic assertion patterns for the Bupkis testing library",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "boneskull"
|
||||
},
|
||||
"skills": [
|
||||
"./skills"
|
||||
],
|
||||
"commands": [
|
||||
"./commands"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# bupkis
|
||||
|
||||
Idiomatic assertion patterns for the Bupkis testing library
|
||||
111
commands/bupkis.md
Normal file
111
commands/bupkis.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
description: Get guidance on writing idiomatic bupkis assertions
|
||||
argument-hint: '[what-to-assert]'
|
||||
---
|
||||
|
||||
# Bupkis Assertion Guidance
|
||||
|
||||
Provide guidance on writing idiomatic assertions using the bupkis testing library.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/bupkis [optional: what you're trying to assert]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
- `what-to-assert` (optional): Describe what you're trying to test or assert
|
||||
|
||||
## Instructions
|
||||
|
||||
When this command is invoked:
|
||||
|
||||
1. **Load the bupkis assertion patterns skill** from `skills/bupkis-assertion-patterns/SKILL.md`
|
||||
|
||||
2. **If the user provided context**, analyze their needs and:
|
||||
- Identify which patterns from the skill apply to their situation
|
||||
- Provide specific code examples using those patterns
|
||||
- Explain why the recommended approach is better
|
||||
- Show before/after comparisons from the skill when helpful
|
||||
|
||||
3. **If no context was provided**, offer to help by:
|
||||
- Asking what they're trying to test or assert
|
||||
- Listing common scenarios:
|
||||
- Checking if properties exist on objects
|
||||
- Verifying object structure and types
|
||||
- Testing arrays or collections
|
||||
- Validating promise behavior
|
||||
- Type checking values
|
||||
- Offering to show examples of specific patterns
|
||||
|
||||
4. **Always emphasize key principles**:
|
||||
- Using semantic assertions over indirect checks
|
||||
- Combining related checks with `to satisfy`
|
||||
- Choosing positive assertions when possible (e.g., `to be defined` vs `not to be undefined`)
|
||||
- Letting assertions imply related checks (e.g., `to be an object` implies non-null)
|
||||
- Using `expectAsync` for promise-based assertions
|
||||
|
||||
## Example Interactions
|
||||
|
||||
**With context:**
|
||||
|
||||
```text
|
||||
User: /bupkis checking if an object has multiple numeric properties
|
||||
|
||||
Agent: For checking multiple properties with specific types, use `to satisfy`
|
||||
with nested `expect.it()` assertions:
|
||||
|
||||
✅ DO - concise, shows structure at a glance:
|
||||
expect(state, 'to satisfy', {
|
||||
filesCompleted: expect.it('to be a number'),
|
||||
suitesCompleted: expect.it('to be a number'),
|
||||
tasksCompleted: expect.it('to be a number'),
|
||||
});
|
||||
|
||||
This is better than separate assertions because:
|
||||
- Shows the expected structure clearly
|
||||
- Better error messages show exactly which properties failed
|
||||
- More maintainable - all related checks in one place
|
||||
```
|
||||
|
||||
**With specific code:**
|
||||
|
||||
```text
|
||||
User: /bupkis
|
||||
How can I improve this?
|
||||
expect('filesCompleted' in state, 'to be truthy');
|
||||
|
||||
Agent: Instead of checking property existence with the `in` operator,
|
||||
use the semantic `to have property` assertion:
|
||||
|
||||
✅ DO - clear, semantic assertion:
|
||||
expect(state, 'to have property', 'filesCompleted');
|
||||
|
||||
This is clearer because it directly expresses your intent and provides
|
||||
better error messages when the property is missing.
|
||||
```
|
||||
|
||||
**Without context:**
|
||||
|
||||
```text
|
||||
User: /bupkis
|
||||
|
||||
Agent: I can help you write idiomatic bupkis assertions! What are you trying to test?
|
||||
|
||||
Common scenarios:
|
||||
- Checking if properties exist on objects
|
||||
- Verifying object structure and types
|
||||
- Testing arrays or collections
|
||||
- Validating promise behavior
|
||||
- Type checking values
|
||||
|
||||
You can also paste your current test code and I'll suggest improvements,
|
||||
or ask about a specific assertion pattern!
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
- [Bupkis Assertion Patterns Skill](../skills/bupkis-assertion-patterns/SKILL.md)
|
||||
- [API Reference](../skills/bupkis-assertion-patterns/references/api_reference.md)
|
||||
- [Common Patterns](../skills/bupkis-assertion-patterns/references/common_patterns.md)
|
||||
65
plugin.lock.json
Normal file
65
plugin.lock.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:boneskull/claude-plugins:plugins/bupkis",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "3efc9f78ac8b680e8eb1401eb87b77e34c032eab",
|
||||
"treeHash": "c6a5230497d739cafe4834d8dcab7d80f7264fb469335b9e1a4dfd667ee484d5",
|
||||
"generatedAt": "2025-11-28T10:14:18.876593Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "bupkis",
|
||||
"description": "Idiomatic assertion patterns for the Bupkis testing library",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "65687d92505c34c02de00f67fe2746409c2d4a437153bfb4783ec2cf23ce53c0"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "11a60005cd71663ccbc11c1a249d31ce987dc76b489b7dd406caec0ad3e8db53"
|
||||
},
|
||||
{
|
||||
"path": "commands/bupkis.md",
|
||||
"sha256": "8263964b5ad5421efb2ddb29dd1b697b37081dbf385333a04634f3da99db89f4"
|
||||
},
|
||||
{
|
||||
"path": "skills/skill-rules.json",
|
||||
"sha256": "01ab72ad5bb5c37b280235b66fdc9f6c04dd015aca474dca0ddfef2a049b5a07"
|
||||
},
|
||||
{
|
||||
"path": "skills/bupkis-assertion-patterns/SKILL.md",
|
||||
"sha256": "dd3dd66346e114476b38bbd7a0754c6e1dc1f206b899072c5e5d3947ba147631"
|
||||
},
|
||||
{
|
||||
"path": "skills/bupkis-assertion-patterns/references/api_reference.md",
|
||||
"sha256": "65129a484f0edc8097a680b65e0d3f4368120f81b939b1ca509d06a8c6e07a7c"
|
||||
},
|
||||
{
|
||||
"path": "skills/bupkis-assertion-patterns/references/common_patterns.md",
|
||||
"sha256": "5154cfb7ebf3b8d52eae37764483a2b8a8409e00b78af7e46d493b24a4daef27"
|
||||
},
|
||||
{
|
||||
"path": "skills/bupkis-assertion-patterns/references/README.md",
|
||||
"sha256": "03d7f9aa88fbe29ec820c952b64dd6f70d8f6435aba230f6b649ff207673bd5b"
|
||||
}
|
||||
],
|
||||
"dirSha256": "c6a5230497d739cafe4834d8dcab7d80f7264fb469335b9e1a4dfd667ee484d5"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
307
skills/bupkis-assertion-patterns/SKILL.md
Normal file
307
skills/bupkis-assertion-patterns/SKILL.md
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
name: bupkis-assertion-patterns
|
||||
description: How to write idiomatic assertions with the Bupkis assertion library for TypeScript and JavaScript
|
||||
---
|
||||
|
||||
# Bupkis Assertion Patterns
|
||||
|
||||
Write idiomatic, expressive assertions using bupkis' powerful assertion vocabulary to make tests more readable and maintainable.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Writing tests with the bupkis assertion library
|
||||
- Checking properties, types, or structure of objects
|
||||
- Verifying arrays or collections
|
||||
- Want clearer, more expressive test failures
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- When a simple, direct assertion is clearer (don't over-complicate)
|
||||
- When the pattern doesn't improve readability
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Use semantic assertions**: Choose assertions that express intent, not implementation
|
||||
2. **Combine related checks**: Use `to satisfy` for multiple properties rather than separate assertions
|
||||
3. **Leverage bupkis vocabulary**: Use built-in assertions like `to have property`, `to be empty`, etc.
|
||||
4. **Let assertions imply related checks**: e.g., `to be an object` already implies non-null
|
||||
|
||||
## Patterns
|
||||
|
||||
### 1. Property Existence
|
||||
|
||||
**Prefer semantic property checks over truthiness checks.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - unclear intent, indirect check
|
||||
expect('filesCompleted' in state, 'to be truthy');
|
||||
|
||||
// ✅ DO - clear, semantic assertion
|
||||
expect(state, 'to have property', 'filesCompleted');
|
||||
```
|
||||
|
||||
**Why:** `to have property` expresses the intent clearly and provides better error messages.
|
||||
|
||||
---
|
||||
|
||||
### 2. Type Checking Multiple Properties
|
||||
|
||||
**Combine related type checks using `to satisfy` instead of separate assertions.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - repetitive, verbose
|
||||
expect(typeof state.filesCompleted, 'to equal', 'number');
|
||||
expect(typeof state.suitesCompleted, 'to equal', 'number');
|
||||
expect(typeof state.tasksCompleted, 'to equal', 'number');
|
||||
|
||||
// ✅ DO - concise, shows structure at a glance
|
||||
expect(state, 'to satisfy', {
|
||||
filesCompleted: expect.it('to be a number'),
|
||||
suitesCompleted: expect.it('to be a number'),
|
||||
tasksCompleted: expect.it('to be a number'),
|
||||
});
|
||||
```
|
||||
|
||||
**Why:** `to satisfy` lets you verify multiple properties in one assertion, showing the expected structure clearly. Better error messages show exactly which properties failed.
|
||||
|
||||
---
|
||||
|
||||
### 3. Non-Empty Collections
|
||||
|
||||
**Use semantic collection assertions instead of length comparisons.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - indirect, requires mental math
|
||||
expect(result.files.length, 'to be greater than', 0);
|
||||
|
||||
// ✅ DO - direct, semantic
|
||||
expect(result.files, 'not to be empty');
|
||||
```
|
||||
|
||||
**Why:** `not to be empty` directly expresses the intent. Works for arrays, strings, objects, Maps, Sets, etc.
|
||||
|
||||
---
|
||||
|
||||
### 4. Object and Null Checks
|
||||
|
||||
**Don't redundantly check for null when object check already implies it.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - redundant null check
|
||||
expect(config, 'to be an object');
|
||||
expect(config, 'not to be null');
|
||||
|
||||
// ✅ DO - object check implies non-null
|
||||
expect(config, 'to be an object'); // already implies non-null
|
||||
```
|
||||
|
||||
**Why:** In bupkis, `to be an object` already ensures the value is not null. Redundant checks add noise.
|
||||
|
||||
---
|
||||
|
||||
### 5. Object Structure Verification
|
||||
|
||||
**Verify object structure with `to satisfy` instead of multiple property assertions.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - fragmented, hard to see expected structure
|
||||
expect(config, 'to be an object');
|
||||
expect(config.iterations, 'to equal', 500);
|
||||
expect(config.reporters[0], 'to equal', 'json');
|
||||
expect(config.reporters.length, 'to equal', 1);
|
||||
|
||||
// ✅ DO - clear, declarative structure check
|
||||
expect(config, 'to satisfy', {
|
||||
iterations: 500,
|
||||
reporters: expect.it('to deep equal', ['json']),
|
||||
});
|
||||
```
|
||||
|
||||
**Why:** `to satisfy` lets you declaratively specify the expected structure. Combines type check and property validation in one assertion. Shows the expected shape at a glance.
|
||||
|
||||
---
|
||||
|
||||
### 5b. Multiple Conditions on the Same Result Object
|
||||
|
||||
**Combine multiple checks on a result object with `to satisfy` instead of separate assertions.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - multiple separate assertions
|
||||
expect(result.exitCode, 'to equal', 0);
|
||||
expect(result.stdout, 'to match', /No historical data/);
|
||||
expect(result.stderr, 'not to match', /toLocaleDateString is not a function/);
|
||||
|
||||
// ✅ DO - single to satisfy assertion
|
||||
expect(result, 'to satisfy', {
|
||||
exitCode: 0,
|
||||
stdout: expect.it('to match', /No historical data/),
|
||||
stderr: expect.it('not to match', /toLocaleDateString is not a function/),
|
||||
});
|
||||
```
|
||||
|
||||
**Why:** Groups all related checks on the same object. Shows the expected result state clearly. Easier to maintain - add/remove checks in one place. Better error messages show exactly which property failed.
|
||||
|
||||
---
|
||||
|
||||
### 6. Defined Value Checks
|
||||
|
||||
**Use positive assertions instead of negated ones when possible.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - negated assertion
|
||||
expect(result, 'not to be undefined');
|
||||
|
||||
// ✅ DO - positive, semantic
|
||||
expect(result, 'to be defined');
|
||||
```
|
||||
|
||||
**Why:** `to be defined` is clearer and more idiomatic than negating undefined. It's a positive assertion that directly expresses intent.
|
||||
|
||||
---
|
||||
|
||||
### 7. Chaining Assertions with 'and'
|
||||
|
||||
**Use concatenation when making multiple assertions on the same subject.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - separate assertions
|
||||
expect(config, 'to be an object'); // implies non-null
|
||||
expect(config, 'to have property', 'reporters');
|
||||
|
||||
// ✅ DO - chain with 'and'
|
||||
expect(config, 'to be an object', 'and', 'to have property', 'reporters');
|
||||
```
|
||||
|
||||
**Why:** Chaining assertions with `'and'` keeps related checks together in a single statement. More concise and shows that you're checking multiple aspects of the same value. Better error messages that show which part of the chain failed.
|
||||
|
||||
---
|
||||
|
||||
### 8. Multiple Property Checks with Array
|
||||
|
||||
**Use `to have properties` with an array when checking for multiple properties.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - chain multiple property checks
|
||||
expect(
|
||||
config,
|
||||
'to be an object',
|
||||
'and',
|
||||
'to have property',
|
||||
'outputDir',
|
||||
'and',
|
||||
'to have property',
|
||||
'reporters',
|
||||
);
|
||||
|
||||
// ✅ DO - use 'to have properties' with array
|
||||
expect(config, 'to have properties', ['outputDir', 'reporters']);
|
||||
```
|
||||
|
||||
**Why:** `to have properties` with an array is specifically designed for checking multiple properties at once. More concise than chaining. Shows all required properties clearly in one place. Better error messages that list all missing properties.
|
||||
|
||||
---
|
||||
|
||||
### 9. Promise Rejection Checks
|
||||
|
||||
**Use `expectAsync` with `'to reject'` for testing promise rejections.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - wishy-washy try/catch
|
||||
try {
|
||||
await configManager.load('nonexistent.config.json');
|
||||
expect(true, 'to be truthy'); // Maybe it works?
|
||||
} catch (error) {
|
||||
expect(error, 'to be an', Error); // Or maybe it throws?
|
||||
expect((error as Error).message, 'not to be empty');
|
||||
}
|
||||
|
||||
// ✅ DO - explicit promise rejection check
|
||||
await expectAsync(configManager.load('nonexistent.config.json'), 'to reject');
|
||||
```
|
||||
|
||||
**Why:** Makes the contract explicit - either it should reject or it shouldn't. No ambiguity. `expectAsync` is specifically designed for promise-based assertions. `'to reject'` clearly expresses that rejection is the expected behavior.
|
||||
|
||||
**Related assertions:**
|
||||
|
||||
- `'to reject'` - promise should be rejected
|
||||
- `'to reject with error satisfying'` - promise should reject with specific error
|
||||
|
||||
---
|
||||
|
||||
## Advanced `to satisfy` Patterns
|
||||
|
||||
### Partial Object Matching
|
||||
|
||||
```typescript
|
||||
// Only check specific properties, ignore others
|
||||
expect(result, 'to satisfy', {
|
||||
status: 'complete',
|
||||
// other properties ignored
|
||||
});
|
||||
```
|
||||
|
||||
### Nested Structure
|
||||
|
||||
```typescript
|
||||
expect(benchmark, 'to satisfy', {
|
||||
name: expect.it('to be a string'),
|
||||
results: expect.it('to satisfy', {
|
||||
mean: expect.it('to be a number'),
|
||||
median: expect.it('to be a number'),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Array Element Values
|
||||
|
||||
```typescript
|
||||
// Check specific array values within to satisfy
|
||||
expect(config, 'to satisfy', {
|
||||
iterations: 100,
|
||||
reporters: ['human'], // checks first element matches
|
||||
});
|
||||
|
||||
// Or check multiple elements
|
||||
expect(config, 'to satisfy', {
|
||||
tags: ['performance', 'critical'],
|
||||
});
|
||||
```
|
||||
|
||||
### Arrays with Patterns
|
||||
|
||||
```typescript
|
||||
// All items must satisfy condition
|
||||
expect(results, 'to have items satisfying', {
|
||||
duration: expect.it('to be a number'),
|
||||
status: 'success',
|
||||
});
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Instead of... | Use... |
|
||||
| -------------------------------------- | ------------------------------------------------------ |
|
||||
| `'prop' in obj, 'to be truthy'` | `obj, 'to have property', 'prop'` |
|
||||
| `typeof x.prop, 'to equal', 'number'` | `x, 'to satisfy', {prop: expect.it('to be a number')}` |
|
||||
| `arr.length, 'to be greater than', 0` | `arr, 'not to be empty'` |
|
||||
| `result, 'not to be undefined'` | `result, 'to be defined'` |
|
||||
| Separate expect() on same subject | Chain with `'and'`: `expect(x, 'a', 'and', 'b')` |
|
||||
| Multiple 'to have property' assertions | `to have properties`, `['prop1', 'prop2']` |
|
||||
| try/catch for promise rejection | `await expectAsync(promise, 'to reject')` |
|
||||
| Multiple assertions on the same object | `to satisfy` with object structure |
|
||||
| Separate object + null checks | Just `to be an object` |
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Clearer intent**: Semantic assertions express what you're testing, not how
|
||||
2. **Better error messages**: bupkis shows exactly what failed in structural checks
|
||||
3. **More maintainable**: Related checks grouped together, easier to update
|
||||
4. **Less code**: Combine multiple assertions into expressive structure checks
|
||||
5. **Discover structure**: `to satisfy` shows expected object shape at a glance
|
||||
|
||||
## Tools Used
|
||||
|
||||
- `expect()` with bupkis assertion vocabulary
|
||||
- `expect.it()` for nested assertions within `to satisfy`
|
||||
- Semantic assertions: `to have property`, `not to be empty`, `to be an object`
|
||||
- Structural assertions: `to satisfy`, `to deep equal`
|
||||
69
skills/bupkis-assertion-patterns/references/README.md
Normal file
69
skills/bupkis-assertion-patterns/references/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Bupkis References
|
||||
|
||||
This directory contains reference documentation for the Bupkis assertion library.
|
||||
|
||||
## Available References
|
||||
|
||||
### 1. api_reference.md - Complete API Documentation
|
||||
|
||||
**Comprehensive reference for all built-in Bupkis assertions** organized by category:
|
||||
|
||||
- **Core Concepts** - Natural language assertions, negation, concatenation, embeddable assertions
|
||||
- **Primitive Assertions** - Type checks (string, number, boolean, null, undefined, etc.)
|
||||
- **Numeric Assertions** - Number comparisons, ranges, special values (NaN, Infinity)
|
||||
- **String & Pattern Assertions** - String matching, RegExp, substring operations
|
||||
- **Collection Assertions** - Arrays, Maps, Sets, WeakMap, WeakSet operations
|
||||
- **Object Assertions** - Property checks, object matching, `to satisfy` patterns
|
||||
- **Function Assertions** - Function type checks, arity, throw behavior
|
||||
- **Equality & Comparison Assertions** - Deep equality, instance checks
|
||||
- **Error Assertions** - Error type and message validation
|
||||
- **Date & Time Assertions** - Date comparisons, durations, weekday/weekend checks
|
||||
- **Promise Assertions** - Async resolution and rejection testing
|
||||
- **Other Assertions** - Truthy, falsy, defined checks
|
||||
|
||||
**When to use:** When you need to look up the exact syntax for an assertion, understand what parameters it accepts, or see all available aliases.
|
||||
|
||||
### 2. common_patterns.md - Practical Usage Patterns
|
||||
|
||||
**Real-world examples and best practices** for common testing scenarios:
|
||||
|
||||
- **API Response Validation** - REST API, pagination, error responses
|
||||
- **Configuration Validation** - App config, environment-specific config, feature flags
|
||||
- **Error Testing Patterns** - Standard errors, custom errors, promise rejections
|
||||
- **Async Operation Patterns** - Promise resolution, async functions, race conditions
|
||||
- **Complex Nested Structures** - Deep object validation, arrays of objects, optional properties
|
||||
- **Test Data Validation** - Factory data, fixtures, mock data consistency
|
||||
- **Type Safety Patterns** - Discriminated unions, generic validation, branded types
|
||||
- **Real-World Scenarios** - Database queries, forms, file system, HTTP headers, events
|
||||
|
||||
**When to use:** When implementing tests and need examples of idiomatic Bupkis patterns for common use cases.
|
||||
|
||||
## How These References Are Used
|
||||
|
||||
When Claude loads this skill and you ask about Bupkis assertions, it will:
|
||||
|
||||
1. **Load SKILL.md first** (always in context) - Provides the core patterns and workflow
|
||||
2. **Load references as needed** - Fetches specific reference files based on your question:
|
||||
- Questions about "what assertions are available" → `api_reference.md`
|
||||
- Questions about "how to test API responses" → `common_patterns.md`
|
||||
- Questions about specific assertion syntax → `api_reference.md`
|
||||
|
||||
This three-level progressive disclosure keeps context usage efficient while ensuring comprehensive information is available when needed.
|
||||
|
||||
## Reference File Organization
|
||||
|
||||
Both reference files use:
|
||||
|
||||
- Clear table of contents for quick navigation
|
||||
- Consistent formatting (heading hierarchy, code blocks)
|
||||
- Success ✓ and failure ✗ examples for each assertion
|
||||
- Inline comments explaining key concepts
|
||||
- Cross-references between related sections
|
||||
|
||||
## Extending These References
|
||||
|
||||
If you discover new patterns or need to document additional use cases:
|
||||
|
||||
1. **Add patterns to common_patterns.md** - Keep examples practical and well-commented
|
||||
2. **Update api_reference.md** - If Bupkis adds new assertions
|
||||
3. **Keep files under 10k words each** - Split into subtopics if growing too large
|
||||
1408
skills/bupkis-assertion-patterns/references/api_reference.md
Normal file
1408
skills/bupkis-assertion-patterns/references/api_reference.md
Normal file
File diff suppressed because it is too large
Load Diff
603
skills/bupkis-assertion-patterns/references/common_patterns.md
Normal file
603
skills/bupkis-assertion-patterns/references/common_patterns.md
Normal file
@@ -0,0 +1,603 @@
|
||||
# Common Bupkis Patterns
|
||||
|
||||
Practical patterns and real-world examples for using Bupkis assertions effectively.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [API Response Validation](#api-response-validation)
|
||||
- [Configuration Validation](#configuration-validation)
|
||||
- [Error Testing Patterns](#error-testing-patterns)
|
||||
- [Async Operation Patterns](#async-operation-patterns)
|
||||
- [Complex Nested Structures](#complex-nested-structures)
|
||||
- [Test Data Validation](#test-data-validation)
|
||||
- [Type Safety Patterns](#type-safety-patterns)
|
||||
|
||||
---
|
||||
|
||||
## API Response Validation
|
||||
|
||||
### REST API Success Response
|
||||
|
||||
```typescript
|
||||
const response = await fetch('/api/users/1');
|
||||
const data = await response.json();
|
||||
|
||||
// Validate response structure
|
||||
expect(data, 'to satisfy', {
|
||||
id: expect.it('to be a number'),
|
||||
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
name: expect.it('to be a string', 'and', 'not to be empty'),
|
||||
createdAt: expect.it('to be a valid date'),
|
||||
roles: expect.it('to be an array', 'and', 'not to be empty'),
|
||||
});
|
||||
```
|
||||
|
||||
### Paginated API Response
|
||||
|
||||
```typescript
|
||||
const response = await fetchPaginatedUsers({ page: 1, limit: 10 });
|
||||
|
||||
expect(response, 'to satisfy', {
|
||||
data: expect.it('to be an array', 'and', 'to have length', 10),
|
||||
meta: {
|
||||
currentPage: 1,
|
||||
totalPages: expect.it('to be a positive integer'),
|
||||
totalItems: expect.it('to be a positive integer'),
|
||||
hasNextPage: expect.it('to be a boolean'),
|
||||
},
|
||||
});
|
||||
|
||||
// Validate each user in the array
|
||||
expect(response.data, 'to have items satisfying', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
name: expect.it('to be a string'),
|
||||
});
|
||||
```
|
||||
|
||||
### Error Response Validation
|
||||
|
||||
```typescript
|
||||
const response = await fetchWithError('/api/invalid');
|
||||
|
||||
expect(response, 'to satisfy', {
|
||||
error: {
|
||||
code: expect.it('to be a string'),
|
||||
message: expect.it('to be a string', 'and', 'not to be empty'),
|
||||
statusCode: expect.it('to be between', 400, 599),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Validation
|
||||
|
||||
### Application Config
|
||||
|
||||
```typescript
|
||||
const config = loadAppConfig();
|
||||
|
||||
expect(config, 'to satisfy', {
|
||||
port: expect.it('to be between', 1024, 65535),
|
||||
host: expect.it('to be a string'),
|
||||
database: {
|
||||
url: /^postgresql:\/\//,
|
||||
poolSize: expect.it('to be a positive integer'),
|
||||
},
|
||||
logging: {
|
||||
level: expect.it('to be one of', ['debug', 'info', 'warn', 'error']),
|
||||
format: expect.it('to be one of', ['json', 'text']),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Environment-Specific Config
|
||||
|
||||
```typescript
|
||||
const prodConfig = loadConfig('production');
|
||||
|
||||
expect(prodConfig, 'to satisfy', {
|
||||
env: 'production',
|
||||
debug: false,
|
||||
apiUrl: /^https:\/\//, // Must be HTTPS in production
|
||||
rateLimiting: {
|
||||
enabled: true,
|
||||
maxRequests: expect.it('to be a positive integer'),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
|
||||
```typescript
|
||||
const featureFlags = getFeatureFlags();
|
||||
|
||||
expect(featureFlags, 'to be an object', 'and', 'not to be empty');
|
||||
|
||||
expect(featureFlags, 'to satisfy', {
|
||||
newDashboard: expect.it('to be a boolean'),
|
||||
betaFeatures: expect.it('to be a boolean'),
|
||||
experimentalApi: expect.it('to be a boolean'),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Testing Patterns
|
||||
|
||||
### Standard Error Handling
|
||||
|
||||
```typescript
|
||||
// Function that should throw
|
||||
expect(() => parseJSON('invalid json'), 'to throw a', SyntaxError);
|
||||
|
||||
// With message matching
|
||||
expect(
|
||||
() => parseJSON('invalid json'),
|
||||
'to throw a',
|
||||
SyntaxError,
|
||||
'satisfying',
|
||||
{ message: /Unexpected token/ },
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Error Properties
|
||||
|
||||
```typescript
|
||||
expect(
|
||||
() => {
|
||||
const err = new Error('Database connection failed');
|
||||
err.code = 'ECONNREFUSED';
|
||||
err.port = 5432;
|
||||
throw err;
|
||||
},
|
||||
'to throw an error satisfying',
|
||||
{
|
||||
message: /connection failed/,
|
||||
code: 'ECONNREFUSED',
|
||||
port: 5432,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Promise Rejection
|
||||
|
||||
```typescript
|
||||
// Simple rejection
|
||||
await expectAsync(fetchData('/api/invalid'), 'to reject');
|
||||
|
||||
// With error type
|
||||
await expectAsync(fetchData('/api/invalid'), 'to reject with a', HttpError);
|
||||
|
||||
// With error properties
|
||||
await expectAsync(
|
||||
fetchData('/api/invalid'),
|
||||
'to reject with error satisfying',
|
||||
{
|
||||
statusCode: 404,
|
||||
message: /not found/,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Try-Catch Alternative
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T use try-catch for expected errors
|
||||
try {
|
||||
await riskyOperation();
|
||||
expect(true, 'to be truthy'); // Maybe it works?
|
||||
} catch (error) {
|
||||
expect(error, 'to be an', Error); // Or maybe it throws?
|
||||
}
|
||||
|
||||
// ✅ DO use expectAsync with explicit contract
|
||||
await expectAsync(riskyOperation(), 'to reject with a', ValidationError);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Operation Patterns
|
||||
|
||||
### Promise Resolution
|
||||
|
||||
```typescript
|
||||
// Basic resolution
|
||||
await expectAsync(fetchUser(123), 'to resolve');
|
||||
|
||||
// With value validation
|
||||
await expectAsync(fetchUser(123), 'to resolve with value satisfying', {
|
||||
id: 123,
|
||||
name: expect.it('to be a string'),
|
||||
email: /^[^\s@]+@/,
|
||||
});
|
||||
```
|
||||
|
||||
### Async Function Testing
|
||||
|
||||
```typescript
|
||||
async function processData(input) {
|
||||
// ... processing
|
||||
return result;
|
||||
}
|
||||
|
||||
await expectAsync(
|
||||
async () => processData({ valid: true }),
|
||||
'to resolve with value satisfying',
|
||||
{ success: true },
|
||||
);
|
||||
|
||||
await expectAsync(
|
||||
async () => processData({ invalid: true }),
|
||||
'to reject with error satisfying',
|
||||
{ code: 'INVALID_INPUT' },
|
||||
);
|
||||
```
|
||||
|
||||
### Race Conditions
|
||||
|
||||
```typescript
|
||||
// Ensure operation completes within timeout
|
||||
const promise = Promise.race([slowOperation(), timeout(5000)]);
|
||||
|
||||
await expectAsync(promise, 'to resolve with value satisfying', {
|
||||
completed: true,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complex Nested Structures
|
||||
|
||||
### Deep Object Validation
|
||||
|
||||
```typescript
|
||||
const benchmark = {
|
||||
name: 'Array.sort performance',
|
||||
date: '2024-01-15',
|
||||
results: {
|
||||
samples: [1.2, 1.3, 1.1, 1.4, 1.2],
|
||||
statistics: {
|
||||
mean: 1.24,
|
||||
median: 1.2,
|
||||
stdDev: 0.11,
|
||||
min: 1.1,
|
||||
max: 1.4,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
platform: 'Node.js v20.10.0',
|
||||
arch: 'x64',
|
||||
},
|
||||
};
|
||||
|
||||
expect(benchmark, 'to satisfy', {
|
||||
name: expect.it('to be a string', 'and', 'not to be empty'),
|
||||
date: /^\d{4}-\d{2}-\d{2}$/,
|
||||
results: {
|
||||
samples: expect.it('to be an array', 'and', 'to be non-empty'),
|
||||
statistics: {
|
||||
mean: expect.it('to be a number', 'and', 'to be positive'),
|
||||
median: expect.it('to be a number', 'and', 'to be positive'),
|
||||
stdDev: expect.it('to be a number'),
|
||||
min: expect.it('to be a number'),
|
||||
max: expect.it('to be a number'),
|
||||
},
|
||||
},
|
||||
metadata: expect.it('to be an object'),
|
||||
});
|
||||
```
|
||||
|
||||
### Array of Complex Objects
|
||||
|
||||
```typescript
|
||||
const transactions = [
|
||||
{ id: 1, amount: 100.5, currency: 'USD', status: 'completed' },
|
||||
{ id: 2, amount: 250.0, currency: 'EUR', status: 'pending' },
|
||||
{ id: 3, amount: 75.25, currency: 'GBP', status: 'completed' },
|
||||
];
|
||||
|
||||
// Validate array structure
|
||||
expect(transactions, 'to be an array', 'and', 'not to be empty');
|
||||
|
||||
// Validate each transaction
|
||||
expect(transactions, 'to have items satisfying', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
amount: expect.it('to be a positive number'),
|
||||
currency: expect.it('to be one of', ['USD', 'EUR', 'GBP']),
|
||||
status: expect.it('to be one of', ['pending', 'completed', 'failed']),
|
||||
});
|
||||
```
|
||||
|
||||
### Optional Properties
|
||||
|
||||
```typescript
|
||||
const user = {
|
||||
id: 123,
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
// phone is optional
|
||||
};
|
||||
|
||||
// Use `to satisfy` to ignore missing optional properties
|
||||
expect(user, 'to satisfy', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
name: expect.it('to be a string'),
|
||||
email: /^[^\s@]+@/,
|
||||
// phone not required, will pass if missing
|
||||
});
|
||||
|
||||
// Explicitly check for optional property if present
|
||||
if ('phone' in user) {
|
||||
expect(user.phone, 'to match', /^\+?\d{10,15}$/);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Data Validation
|
||||
|
||||
### Factory-Generated Data
|
||||
|
||||
```typescript
|
||||
const user = UserFactory.create();
|
||||
|
||||
// Validate factory output
|
||||
expect(user, 'to satisfy', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
createdAt: expect.it('to be a Date'),
|
||||
roles: expect.it('to be an array', 'and', 'not to be empty'),
|
||||
});
|
||||
```
|
||||
|
||||
### Fixture Data
|
||||
|
||||
```typescript
|
||||
const fixtures = loadFixtures('users.json');
|
||||
|
||||
expect(fixtures, 'to be an array', 'and', 'to be non-empty');
|
||||
|
||||
// Validate all fixtures
|
||||
expect(fixtures, 'to have items satisfying', {
|
||||
id: expect.it('to be a number'),
|
||||
email: expect.it('to be a string'),
|
||||
name: expect.it('to be a string'),
|
||||
});
|
||||
```
|
||||
|
||||
### Mock Data Consistency
|
||||
|
||||
```typescript
|
||||
const mockResponse = {
|
||||
users: [
|
||||
/* ... */
|
||||
],
|
||||
meta: { total: 50 },
|
||||
};
|
||||
|
||||
// Ensure mock data is consistent
|
||||
expect(
|
||||
mockResponse.users.length,
|
||||
'to be less than or equal to',
|
||||
mockResponse.meta.total,
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type Safety Patterns
|
||||
|
||||
### Discriminated Unions
|
||||
|
||||
```typescript
|
||||
type SuccessResult = { success: true; data: any };
|
||||
type ErrorResult = { success: false; error: string };
|
||||
type Result = SuccessResult | ErrorResult;
|
||||
|
||||
const result: Result = processOperation();
|
||||
|
||||
if (result.success) {
|
||||
expect(result, 'to satisfy', {
|
||||
success: true,
|
||||
data: expect.it('to be defined'),
|
||||
});
|
||||
} else {
|
||||
expect(result, 'to satisfy', {
|
||||
success: false,
|
||||
error: expect.it('to be a string', 'and', 'not to be empty'),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Generic Type Validation
|
||||
|
||||
```typescript
|
||||
function validateResponse<T>(
|
||||
response: unknown,
|
||||
shape: Record<string, any>,
|
||||
): asserts response is T {
|
||||
expect(response, 'to satisfy', shape);
|
||||
}
|
||||
|
||||
// Usage
|
||||
const response = await fetchData();
|
||||
validateResponse<User>(response, {
|
||||
id: expect.it('to be a number'),
|
||||
email: expect.it('to be a string'),
|
||||
});
|
||||
|
||||
// TypeScript now knows response is User
|
||||
console.log(response.email);
|
||||
```
|
||||
|
||||
### Branded Types
|
||||
|
||||
```typescript
|
||||
type UserId = number & { __brand: 'UserId' };
|
||||
|
||||
function assertUserId(value: number): asserts value is UserId {
|
||||
expect(value, 'to be a positive integer');
|
||||
}
|
||||
|
||||
const id: number = 123;
|
||||
assertUserId(id);
|
||||
// id is now UserId
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Real-World Scenarios
|
||||
|
||||
### Database Query Results
|
||||
|
||||
```typescript
|
||||
const users = await db.query('SELECT * FROM users WHERE active = true');
|
||||
|
||||
expect(users, 'to be an array');
|
||||
expect(users, 'to have items satisfying', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
email: expect.it('to be a string'),
|
||||
active: true,
|
||||
created_at: expect.it('to be a Date'),
|
||||
});
|
||||
```
|
||||
|
||||
### Form Validation
|
||||
|
||||
```typescript
|
||||
const formData = {
|
||||
username: 'alice123',
|
||||
email: 'alice@example.com',
|
||||
password: 'securePassword123!',
|
||||
age: 25,
|
||||
};
|
||||
|
||||
expect(formData, 'to satisfy', {
|
||||
username: expect.it('to be a string', 'and', 'to have length between', 3, 20),
|
||||
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
password: expect.it('to be a string', 'and', 'to have length at least', 8),
|
||||
age: expect.it('to be between', 13, 120),
|
||||
});
|
||||
```
|
||||
|
||||
### File System Operations
|
||||
|
||||
```typescript
|
||||
const stats = await fs.stat('package.json');
|
||||
|
||||
expect(stats, 'to satisfy', {
|
||||
size: expect.it('to be a positive number'),
|
||||
isFile: expect.it('to be a function'),
|
||||
isDirectory: expect.it('to be a function'),
|
||||
mtime: expect.it('to be a Date'),
|
||||
});
|
||||
|
||||
expect(stats.isFile(), 'to be true');
|
||||
```
|
||||
|
||||
### HTTP Headers Validation
|
||||
|
||||
```typescript
|
||||
const headers = response.headers;
|
||||
|
||||
expect(headers, 'to satisfy', {
|
||||
'content-type': /^application\/json/,
|
||||
'cache-control': expect.it('to be a string'),
|
||||
'x-request-id': expect.it('to be a string', 'and', 'not to be empty'),
|
||||
});
|
||||
|
||||
// Check CORS headers
|
||||
expect(headers, 'to have property', 'access-control-allow-origin');
|
||||
```
|
||||
|
||||
### Event Emission Validation
|
||||
|
||||
```typescript
|
||||
const emitter = new EventEmitter();
|
||||
const events: any[] = [];
|
||||
|
||||
emitter.on('data', (data) => events.push(data));
|
||||
|
||||
emitter.emit('data', { id: 1, value: 'test' });
|
||||
emitter.emit('data', { id: 2, value: 'test2' });
|
||||
|
||||
expect(events, 'to have length', 2);
|
||||
expect(events, 'to have items satisfying', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
value: expect.it('to be a string'),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Don't: Overly Specific Assertions
|
||||
|
||||
```typescript
|
||||
// Too brittle - breaks if order changes
|
||||
expect(users, 'to deep equal', [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
]);
|
||||
```
|
||||
|
||||
### ✅ Do: Use Partial Matching
|
||||
|
||||
```typescript
|
||||
// More flexible - only checks what matters
|
||||
expect(users, 'to have items satisfying', {
|
||||
id: expect.it('to be a number'),
|
||||
name: expect.it('to be a string'),
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ Don't: Redundant Checks
|
||||
|
||||
```typescript
|
||||
// Redundant - `to be an object` already implies non-null
|
||||
expect(config, 'to be an object');
|
||||
expect(config, 'not to be null'); // Unnecessary!
|
||||
```
|
||||
|
||||
### ✅ Do: Rely on Implied Checks
|
||||
|
||||
```typescript
|
||||
// Object check already ensures non-null
|
||||
expect(config, 'to be an object');
|
||||
```
|
||||
|
||||
### ❌ Don't: Multiple Separate Assertions
|
||||
|
||||
```typescript
|
||||
// Fragmented - hard to see the expected structure
|
||||
expect(result.exitCode, 'to equal', 0);
|
||||
expect(result.stdout, 'to match', /success/);
|
||||
expect(result.stderr, 'to be empty');
|
||||
```
|
||||
|
||||
### ✅ Do: Use `to satisfy` for Related Properties
|
||||
|
||||
```typescript
|
||||
// Clear - shows expected structure at a glance
|
||||
expect(result, 'to satisfy', {
|
||||
exitCode: 0,
|
||||
stdout: /success/,
|
||||
stderr: expect.it('to be empty'),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips for Writing Better Assertions
|
||||
|
||||
1. **Use `to satisfy` for complex objects** - It's more maintainable than multiple assertions
|
||||
2. **Leverage `expect.it()` for nested validation** - Embed assertions within object patterns
|
||||
3. **Use RegExp for flexible string matching** - More robust than exact string comparisons
|
||||
4. **Chain related assertions with 'and'** - Keeps related checks together
|
||||
5. **Prefer semantic assertions** - Use `to have property` instead of `'key' in obj`
|
||||
6. **Use `to have properties` for multiple keys** - More concise than chaining
|
||||
7. **Let assertions imply related checks** - Don't redundantly check for null after object check
|
||||
8. **Use `expectAsync` for promises** - Makes contract explicit: resolve or reject
|
||||
48
skills/skill-rules.json
Normal file
48
skills/skill-rules.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"description": "Skill activation rules for bupkis plugin",
|
||||
"skills": {
|
||||
"bupkis@boneskull-plugins:bupkis-assertion-patterns": {
|
||||
"type": "domain",
|
||||
"enforcement": "suggest",
|
||||
"priority": "high",
|
||||
"description": "Write idiomatic assertions using bupkis assertion library patterns",
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"bupkis",
|
||||
"assertion",
|
||||
"expect",
|
||||
"test assertion",
|
||||
"bupkis pattern",
|
||||
"to satisfy",
|
||||
"to have property",
|
||||
"write test"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"(write|create|add).*test",
|
||||
"(write|use|add).*assertion",
|
||||
"bupkis.*assertion",
|
||||
"expect.*to.*satisfy",
|
||||
"(how to|how do I).*bupkis"
|
||||
]
|
||||
},
|
||||
"fileTriggers": {
|
||||
"pathPatterns": [
|
||||
"**/*.test.ts",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.ts",
|
||||
"**/*.spec.js",
|
||||
"**/test/**/*.ts",
|
||||
"**/test/**/*.js"
|
||||
],
|
||||
"contentPatterns": [
|
||||
"from 'bupkis'",
|
||||
"from \"bupkis\"",
|
||||
"require\\('bupkis'\\)",
|
||||
"expect\\(.*'to satisfy'",
|
||||
"expect\\(.*'to have property'"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": "1.0"
|
||||
}
|
||||
Reference in New Issue
Block a user