@jaypie/mcp 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aws-B3dW_-bD.js +1202 -0
- package/dist/aws-B3dW_-bD.js.map +1 -0
- package/dist/index.js +166 -1209
- package/dist/index.js.map +1 -1
- package/dist/suite.d.ts +1 -0
- package/dist/suite.js +1252 -0
- package/dist/suite.js.map +1 -0
- package/package.json +8 -2
- package/prompts/Jaypie_Fabric_Package.md +86 -0
- package/release-notes/constructs/1.2.17.md +11 -0
- package/release-notes/fabric/0.1.2.md +11 -0
- package/release-notes/mcp/0.3.3.md +12 -0
- package/release-notes/mcp/0.3.4.md +36 -0
- package/skills/agents.md +25 -0
- package/skills/aws.md +107 -0
- package/skills/cdk.md +141 -0
- package/skills/cicd.md +152 -0
- package/skills/datadog.md +129 -0
- package/skills/debugging.md +148 -0
- package/skills/dns.md +134 -0
- package/skills/dynamodb.md +140 -0
- package/skills/errors.md +142 -0
- package/skills/fabric.md +164 -0
- package/skills/index.md +7 -0
- package/skills/jaypie.md +100 -0
- package/skills/legacy.md +97 -0
- package/skills/logs.md +160 -0
- package/skills/mocks.md +174 -0
- package/skills/models.md +195 -0
- package/skills/releasenotes.md +94 -0
- package/skills/secrets.md +155 -0
- package/skills/services.md +175 -0
- package/skills/style.md +190 -0
- package/skills/tests.md +209 -0
- package/skills/tools.md +127 -0
- package/skills/topics.md +116 -0
- package/skills/variables.md +146 -0
- package/skills/writing.md +153 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Secret management with AWS Secrets Manager
|
|
3
|
+
related: aws, cdk, variables
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Secret Management
|
|
7
|
+
|
|
8
|
+
Jaypie uses AWS Secrets Manager for secure credential storage.
|
|
9
|
+
|
|
10
|
+
## Basic Usage
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { getSecret } from "jaypie";
|
|
14
|
+
|
|
15
|
+
const apiKey = await getSecret("my-api-key");
|
|
16
|
+
const dbUri = await getSecret("mongodb-connection-string");
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Environment Variables
|
|
20
|
+
|
|
21
|
+
Reference secrets via environment variables in CDK:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const handler = new JaypieLambda(this, "Handler", {
|
|
25
|
+
environment: {
|
|
26
|
+
SECRET_MONGODB_URI: "mongodb-connection-string",
|
|
27
|
+
SECRET_API_KEY: "third-party-api-key",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
In code:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
const secretName = process.env.SECRET_MONGODB_URI;
|
|
36
|
+
const mongoUri = await getSecret(secretName);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Creating Secrets
|
|
40
|
+
|
|
41
|
+
### Via CDK
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { Secret } from "aws-cdk-lib/aws-secretsmanager";
|
|
45
|
+
|
|
46
|
+
const secret = new Secret(this, "ApiKey", {
|
|
47
|
+
secretName: `${projectKey}/api-key`,
|
|
48
|
+
description: "Third-party API key",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Grant read access
|
|
52
|
+
secret.grantRead(lambdaFunction);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Via AWS CLI
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
aws secretsmanager create-secret \
|
|
59
|
+
--name "my-project/api-key" \
|
|
60
|
+
--secret-string "sk_live_abc123"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Secret Naming Convention
|
|
64
|
+
|
|
65
|
+
Use project-prefixed names:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
{project-key}/{secret-name}
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
- my-api/mongodb-uri
|
|
72
|
+
- my-api/stripe-key
|
|
73
|
+
- my-api/auth0-secret
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## JSON Secrets
|
|
77
|
+
|
|
78
|
+
Store structured data:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
aws secretsmanager create-secret \
|
|
82
|
+
--name "my-project/db-credentials" \
|
|
83
|
+
--secret-string '{"username":"admin","password":"secret123"}'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Retrieve in code:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const credentialsJson = await getSecret("my-project/db-credentials");
|
|
90
|
+
const credentials = JSON.parse(credentialsJson);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Caching
|
|
94
|
+
|
|
95
|
+
Secrets are cached by default to reduce API calls:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// First call: fetches from Secrets Manager
|
|
99
|
+
const key1 = await getSecret("api-key");
|
|
100
|
+
|
|
101
|
+
// Second call: returns cached value
|
|
102
|
+
const key2 = await getSecret("api-key");
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Cache is scoped to Lambda execution context (warm starts reuse cache).
|
|
106
|
+
|
|
107
|
+
## Rotation
|
|
108
|
+
|
|
109
|
+
Configure automatic rotation for supported secrets:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const secret = new Secret(this, "DbPassword", {
|
|
113
|
+
secretName: "my-project/db-password",
|
|
114
|
+
generateSecretString: {
|
|
115
|
+
excludePunctuation: true,
|
|
116
|
+
passwordLength: 32,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
secret.addRotationSchedule("Rotation", {
|
|
121
|
+
automaticallyAfter: Duration.days(30),
|
|
122
|
+
rotationLambda: rotationFunction,
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Local Development
|
|
127
|
+
|
|
128
|
+
For local development, use environment variables:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# .env.local (not committed)
|
|
132
|
+
MONGODB_URI=mongodb://localhost:27017/dev
|
|
133
|
+
API_KEY=test_key_123
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
In code, check for direct value first:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const mongoUri = process.env.MONGODB_URI || await getSecret(process.env.SECRET_MONGODB_URI);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## IAM Permissions
|
|
143
|
+
|
|
144
|
+
Lambda needs `secretsmanager:GetSecretValue`:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
secret.grantRead(lambdaFunction);
|
|
148
|
+
|
|
149
|
+
// Or via policy
|
|
150
|
+
lambdaFunction.addToRolePolicy(new PolicyStatement({
|
|
151
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
152
|
+
resources: [secret.secretArn],
|
|
153
|
+
}));
|
|
154
|
+
```
|
|
155
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Service layer patterns and architecture
|
|
3
|
+
related: fabric, models, tests
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Service Layer Patterns
|
|
7
|
+
|
|
8
|
+
Organizing business logic in Jaypie applications.
|
|
9
|
+
|
|
10
|
+
## Service Structure
|
|
11
|
+
|
|
12
|
+
Keep business logic in service modules:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/
|
|
16
|
+
├── handlers/ # Lambda/Express handlers
|
|
17
|
+
├── services/ # Business logic
|
|
18
|
+
│ ├── user.ts
|
|
19
|
+
│ └── order.ts
|
|
20
|
+
├── models/ # Data models
|
|
21
|
+
└── utils/ # Utilities
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Basic Service Pattern
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// services/user.ts
|
|
28
|
+
import { log, NotFoundError, BadRequestError } from "jaypie";
|
|
29
|
+
import { User } from "../models/user.js";
|
|
30
|
+
|
|
31
|
+
export async function getUser(userId: string) {
|
|
32
|
+
log.debug("Getting user", { userId });
|
|
33
|
+
|
|
34
|
+
const user = await User.findById(userId);
|
|
35
|
+
if (!user) {
|
|
36
|
+
throw new NotFoundError(`User ${userId} not found`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return user;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function createUser(input: UserCreateInput) {
|
|
43
|
+
log.info("Creating user", { email: input.email });
|
|
44
|
+
|
|
45
|
+
const existing = await User.findOne({ email: input.email });
|
|
46
|
+
if (existing) {
|
|
47
|
+
throw new BadRequestError("Email already registered");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return User.create(input);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Handler Integration
|
|
55
|
+
|
|
56
|
+
Handlers call services:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// handlers/user.ts
|
|
60
|
+
import { lambdaHandler } from "@jaypie/lambda";
|
|
61
|
+
import { getUser, createUser } from "../services/user.js";
|
|
62
|
+
|
|
63
|
+
export const getUserHandler = lambdaHandler(async (event) => {
|
|
64
|
+
const { userId } = event.pathParameters;
|
|
65
|
+
return getUser(userId);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const createUserHandler = lambdaHandler(async (event) => {
|
|
69
|
+
const input = JSON.parse(event.body);
|
|
70
|
+
return createUser(input);
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Service Dependencies
|
|
75
|
+
|
|
76
|
+
Inject dependencies for testability:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// services/notification.ts
|
|
80
|
+
import { log } from "jaypie";
|
|
81
|
+
|
|
82
|
+
export interface NotificationService {
|
|
83
|
+
sendEmail(to: string, subject: string, body: string): Promise<void>;
|
|
84
|
+
sendSms(to: string, message: string): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createNotificationService(
|
|
88
|
+
emailClient: EmailClient,
|
|
89
|
+
smsClient: SmsClient
|
|
90
|
+
): NotificationService {
|
|
91
|
+
return {
|
|
92
|
+
async sendEmail(to, subject, body) {
|
|
93
|
+
log.info("Sending email", { to, subject });
|
|
94
|
+
await emailClient.send({ to, subject, body });
|
|
95
|
+
},
|
|
96
|
+
async sendSms(to, message) {
|
|
97
|
+
log.info("Sending SMS", { to });
|
|
98
|
+
await smsClient.send({ to, message });
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Transaction Patterns
|
|
105
|
+
|
|
106
|
+
For DynamoDB operations that must succeed together, use TransactWriteItems:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { TransactWriteItemsCommand } from "@aws-sdk/client-dynamodb";
|
|
110
|
+
|
|
111
|
+
export async function transferFunds(fromId: string, toId: string, amount: number) {
|
|
112
|
+
const from = await getAccount(fromId);
|
|
113
|
+
|
|
114
|
+
if (from.balance < amount) {
|
|
115
|
+
throw new BadRequestError("Insufficient funds");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const command = new TransactWriteItemsCommand({
|
|
119
|
+
TransactItems: [
|
|
120
|
+
{
|
|
121
|
+
Update: {
|
|
122
|
+
TableName: TABLE_NAME,
|
|
123
|
+
Key: { pk: { S: `ACCOUNT#${fromId}` }, sk: { S: "BALANCE" } },
|
|
124
|
+
UpdateExpression: "SET balance = balance - :amount",
|
|
125
|
+
ExpressionAttributeValues: { ":amount": { N: String(amount) } },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
Update: {
|
|
130
|
+
TableName: TABLE_NAME,
|
|
131
|
+
Key: { pk: { S: `ACCOUNT#${toId}` }, sk: { S: "BALANCE" } },
|
|
132
|
+
UpdateExpression: "SET balance = balance + :amount",
|
|
133
|
+
ExpressionAttributeValues: { ":amount": { N: String(amount) } },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await dynamoClient.send(command);
|
|
140
|
+
log.info("Transfer completed", { fromId, toId, amount });
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Service Testing
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
148
|
+
import { getUser } from "./user.js";
|
|
149
|
+
import { User } from "../models/user.js";
|
|
150
|
+
|
|
151
|
+
vi.mock("../models/user.js");
|
|
152
|
+
|
|
153
|
+
describe("getUser", () => {
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
vi.clearAllMocks();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns user when found", async () => {
|
|
159
|
+
const mockUser = { id: "123", name: "John" };
|
|
160
|
+
vi.mocked(User.findById).mockResolvedValue(mockUser);
|
|
161
|
+
|
|
162
|
+
const result = await getUser("123");
|
|
163
|
+
|
|
164
|
+
expect(result).toEqual(mockUser);
|
|
165
|
+
expect(User.findById).toHaveBeenCalledWith("123");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("throws NotFoundError when missing", async () => {
|
|
169
|
+
vi.mocked(User.findById).mockResolvedValue(null);
|
|
170
|
+
|
|
171
|
+
await expect(getUser("123")).rejects.toThrow(NotFoundError);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
package/skills/style.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Code style conventions and patterns
|
|
3
|
+
related: errors, tests
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Style
|
|
7
|
+
|
|
8
|
+
Jaypie coding conventions and patterns.
|
|
9
|
+
|
|
10
|
+
## General Rules
|
|
11
|
+
|
|
12
|
+
### TypeScript Everywhere
|
|
13
|
+
|
|
14
|
+
Use TypeScript for all code:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// GOOD
|
|
18
|
+
function greet(name: string): string {
|
|
19
|
+
return `Hello, ${name}!`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// BAD
|
|
23
|
+
function greet(name) {
|
|
24
|
+
return `Hello, ${name}!`;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### ESM Over CommonJS
|
|
29
|
+
|
|
30
|
+
Use ES modules:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// GOOD
|
|
34
|
+
import { log } from "jaypie";
|
|
35
|
+
export function myFunction() {}
|
|
36
|
+
|
|
37
|
+
// BAD
|
|
38
|
+
const { log } = require("jaypie");
|
|
39
|
+
module.exports = { myFunction };
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Alphabetize Everything
|
|
43
|
+
|
|
44
|
+
Alphabetize imports, object keys, exports:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// GOOD
|
|
48
|
+
import { ConfigurationError, log, NotFoundError } from "jaypie";
|
|
49
|
+
|
|
50
|
+
const config = {
|
|
51
|
+
apiKey: "...",
|
|
52
|
+
baseUrl: "...",
|
|
53
|
+
timeout: 5000,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// BAD
|
|
57
|
+
import { NotFoundError, log, ConfigurationError } from "jaypie";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Function Signatures
|
|
61
|
+
|
|
62
|
+
### Object Parameters
|
|
63
|
+
|
|
64
|
+
Use destructured objects for multiple parameters:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// GOOD
|
|
68
|
+
function createUser({ email, name, role }: CreateUserInput) {}
|
|
69
|
+
|
|
70
|
+
// GOOD (single required + optional config)
|
|
71
|
+
function fetchData(url: string, { timeout, retries }: FetchOptions = {}) {}
|
|
72
|
+
|
|
73
|
+
// BAD (multiple positional parameters)
|
|
74
|
+
function createUser(email: string, name: string, role: string) {}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Allow Zero Arguments
|
|
78
|
+
|
|
79
|
+
Functions with optional config should allow no arguments:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// GOOD
|
|
83
|
+
function initialize(options: InitOptions = {}) {
|
|
84
|
+
const { timeout = 5000, retries = 3 } = options;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Call with or without args
|
|
88
|
+
initialize();
|
|
89
|
+
initialize({ timeout: 10000 });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Constants
|
|
93
|
+
|
|
94
|
+
### File-Level Constants
|
|
95
|
+
|
|
96
|
+
Use SCREAMING_SNAKE_CASE for constants:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
100
|
+
const DATADOG_SITE = "datadoghq.com";
|
|
101
|
+
const HTTP_STATUS = {
|
|
102
|
+
OK: 200,
|
|
103
|
+
NOT_FOUND: 404,
|
|
104
|
+
} as const;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Magic Numbers
|
|
108
|
+
|
|
109
|
+
Never use magic numbers inline:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// BAD
|
|
113
|
+
if (retries > 3) { ... }
|
|
114
|
+
await sleep(5000);
|
|
115
|
+
|
|
116
|
+
// GOOD
|
|
117
|
+
const MAX_RETRIES = 3;
|
|
118
|
+
const RETRY_DELAY_MS = 5000;
|
|
119
|
+
|
|
120
|
+
if (retries > MAX_RETRIES) { ... }
|
|
121
|
+
await sleep(RETRY_DELAY_MS);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Error Handling
|
|
125
|
+
|
|
126
|
+
### Never Vanilla Error
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// BAD
|
|
130
|
+
throw new Error("Missing config");
|
|
131
|
+
|
|
132
|
+
// GOOD
|
|
133
|
+
import { ConfigurationError } from "jaypie";
|
|
134
|
+
throw new ConfigurationError("Missing config");
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Error Context
|
|
138
|
+
|
|
139
|
+
Include relevant context:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
throw new NotFoundError("User not found", {
|
|
143
|
+
context: { userId, searchedAt: new Date() }
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Avoid Over-Engineering
|
|
148
|
+
|
|
149
|
+
### No Premature Abstraction
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// BAD - Unnecessary abstraction for one use
|
|
153
|
+
const formatName = (name) => name.toUpperCase();
|
|
154
|
+
const processUser = (user) => ({ ...user, name: formatName(user.name) });
|
|
155
|
+
|
|
156
|
+
// GOOD - Simple and direct
|
|
157
|
+
const processUser = (user) => ({ ...user, name: user.name.toUpperCase() });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Minimal Changes
|
|
161
|
+
|
|
162
|
+
Only modify what's requested:
|
|
163
|
+
|
|
164
|
+
- Bug fix? Fix the bug, nothing else.
|
|
165
|
+
- Add feature? Add only that feature.
|
|
166
|
+
- Don't add docstrings to unchanged code.
|
|
167
|
+
- Don't refactor surrounding code.
|
|
168
|
+
|
|
169
|
+
## Naming Conventions
|
|
170
|
+
|
|
171
|
+
| Type | Convention | Example |
|
|
172
|
+
|------|-----------|---------|
|
|
173
|
+
| Functions | camelCase | `getUser`, `createOrder` |
|
|
174
|
+
| Classes | PascalCase | `UserService`, `OrderModel` |
|
|
175
|
+
| Constants | SCREAMING_SNAKE | `MAX_RETRIES`, `API_URL` |
|
|
176
|
+
| Files | kebab-case | `user-service.ts`, `order-model.ts` |
|
|
177
|
+
| Types/Interfaces | PascalCase | `UserInput`, `IUserService` |
|
|
178
|
+
|
|
179
|
+
## Lint Rules
|
|
180
|
+
|
|
181
|
+
Use `@jaypie/eslint`:
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
// eslint.config.mjs
|
|
185
|
+
import jaypie from "@jaypie/eslint";
|
|
186
|
+
export default [...jaypie];
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Always run `npm run format` before committing.
|
|
190
|
+
|
package/skills/tests.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Testing patterns with Vitest
|
|
3
|
+
related: mocks, errors
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing Patterns
|
|
7
|
+
|
|
8
|
+
Jaypie uses Vitest for testing with specific patterns and mocks.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
### vitest.config.ts
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { defineConfig } from "vitest/config";
|
|
16
|
+
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
test: {
|
|
19
|
+
coverage: {
|
|
20
|
+
reporter: ["text", "json", "html"],
|
|
21
|
+
},
|
|
22
|
+
globals: true,
|
|
23
|
+
setupFiles: ["./vitest.setup.ts"],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### vitest.setup.ts
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { vi } from "vitest";
|
|
32
|
+
|
|
33
|
+
vi.mock("jaypie", async () => {
|
|
34
|
+
const { mockJaypie } = await import("@jaypie/testkit");
|
|
35
|
+
return mockJaypie(vi);
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Test Structure
|
|
40
|
+
|
|
41
|
+
### File Organization
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
src/
|
|
45
|
+
├── services/
|
|
46
|
+
│ └── user.ts
|
|
47
|
+
└── __tests__/
|
|
48
|
+
└── user.spec.ts
|
|
49
|
+
|
|
50
|
+
# Or co-located:
|
|
51
|
+
src/
|
|
52
|
+
└── services/
|
|
53
|
+
├── user.ts
|
|
54
|
+
└── user.spec.ts
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Test File Pattern
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
61
|
+
|
|
62
|
+
describe("UserService", () => {
|
|
63
|
+
describe("getUser", () => {
|
|
64
|
+
it("returns user when found", async () => {
|
|
65
|
+
// Arrange
|
|
66
|
+
const mockUser = { id: "123", name: "John" };
|
|
67
|
+
vi.mocked(User.findById).mockResolvedValue(mockUser);
|
|
68
|
+
|
|
69
|
+
// Act
|
|
70
|
+
const result = await getUser("123");
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
expect(result).toEqual(mockUser);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Running Tests
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Run all tests (non-watch mode)
|
|
83
|
+
npm test
|
|
84
|
+
|
|
85
|
+
# Run specific package
|
|
86
|
+
npm test -w packages/my-package
|
|
87
|
+
|
|
88
|
+
# Watch mode (development)
|
|
89
|
+
npx vitest
|
|
90
|
+
|
|
91
|
+
# With coverage
|
|
92
|
+
npm test -- --coverage
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Important**: Always use `npm test` or `vitest run`, not bare `vitest` which runs in watch mode.
|
|
96
|
+
|
|
97
|
+
## Mocking
|
|
98
|
+
|
|
99
|
+
### Module Mocks
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
vi.mock("./user-service.js", () => ({
|
|
103
|
+
getUser: vi.fn(),
|
|
104
|
+
createUser: vi.fn(),
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
// Access mocked functions
|
|
108
|
+
import { getUser } from "./user-service.js";
|
|
109
|
+
|
|
110
|
+
vi.mocked(getUser).mockResolvedValue({ id: "123" });
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Jaypie Mocks
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { log, getSecret } from "jaypie";
|
|
117
|
+
|
|
118
|
+
it("logs the operation", async () => {
|
|
119
|
+
await myFunction();
|
|
120
|
+
expect(log.info).toHaveBeenCalledWith("Operation started");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("uses secrets", async () => {
|
|
124
|
+
vi.mocked(getSecret).mockResolvedValue("test-key");
|
|
125
|
+
await myFunction();
|
|
126
|
+
expect(getSecret).toHaveBeenCalledWith("api-key");
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Testing Errors
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { NotFoundError } from "jaypie";
|
|
134
|
+
|
|
135
|
+
it("throws NotFoundError when user missing", async () => {
|
|
136
|
+
vi.mocked(User.findById).mockResolvedValue(null);
|
|
137
|
+
|
|
138
|
+
await expect(getUser("invalid")).rejects.toThrow(NotFoundError);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("includes error context", async () => {
|
|
142
|
+
vi.mocked(User.findById).mockResolvedValue(null);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await getUser("invalid");
|
|
146
|
+
expect.fail("Should have thrown");
|
|
147
|
+
} catch (error) {
|
|
148
|
+
expect(error.context.userId).toBe("invalid");
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Async Testing
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
it("handles async operations", async () => {
|
|
157
|
+
const result = await asyncFunction();
|
|
158
|
+
expect(result).toBeDefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("handles promises", () => {
|
|
162
|
+
return expect(asyncFunction()).resolves.toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("handles rejections", () => {
|
|
166
|
+
return expect(failingFunction()).rejects.toThrow();
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Before/After Hooks
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
describe("UserService", () => {
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
vi.clearAllMocks();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
afterEach(() => {
|
|
179
|
+
vi.resetAllMocks();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
beforeAll(async () => {
|
|
183
|
+
// One-time setup
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
afterAll(async () => {
|
|
187
|
+
// One-time cleanup
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Snapshot Testing
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
it("renders expected output", () => {
|
|
196
|
+
const result = buildConfig({ env: "production" });
|
|
197
|
+
expect(result).toMatchSnapshot();
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Test Coverage
|
|
202
|
+
|
|
203
|
+
Aim for meaningful coverage, not 100%:
|
|
204
|
+
|
|
205
|
+
- Test business logic thoroughly
|
|
206
|
+
- Test error paths
|
|
207
|
+
- Skip trivial getters/setters
|
|
208
|
+
- Skip generated code
|
|
209
|
+
|