@lousy-agents/cli 1.1.0 → 2.0.0
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/README.md +11 -12
- package/api/copilot-with-fastify/.devcontainer/devcontainer.json +90 -0
- package/api/copilot-with-fastify/.editorconfig +16 -0
- package/api/copilot-with-fastify/.github/ISSUE_TEMPLATE/feature-to-spec.yml +55 -0
- package/api/copilot-with-fastify/.github/copilot-instructions.md +387 -0
- package/api/copilot-with-fastify/.github/instructions/pipeline.instructions.md +149 -0
- package/api/copilot-with-fastify/.github/instructions/software-architecture.instructions.md +430 -0
- package/api/copilot-with-fastify/.github/instructions/spec.instructions.md +411 -0
- package/api/copilot-with-fastify/.github/instructions/test.instructions.md +268 -0
- package/api/copilot-with-fastify/.github/specs/README.md +84 -0
- package/api/copilot-with-fastify/.github/workflows/assign-copilot.yml +59 -0
- package/api/copilot-with-fastify/.github/workflows/ci.yml +88 -0
- package/api/copilot-with-fastify/.nvmrc +1 -0
- package/api/copilot-with-fastify/.vscode/extensions.json +14 -0
- package/api/copilot-with-fastify/.vscode/launch.json +30 -0
- package/api/copilot-with-fastify/.vscode/mcp.json +19 -0
- package/api/copilot-with-fastify/.yamllint +18 -0
- package/api/copilot-with-fastify/biome.json +31 -0
- package/api/copilot-with-fastify/package.json +37 -0
- package/api/copilot-with-fastify/tsconfig.json +34 -0
- package/api/copilot-with-fastify/vitest.config.ts +21 -0
- package/api/copilot-with-fastify/vitest.integration.config.ts +18 -0
- package/api/copilot-with-fastify/vitest.setup.ts +5 -0
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +39 -45
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/config.d.ts +6 -5
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +186 -6
- package/dist/lib/config.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ npx @lousy-agents/cli copilot-setup
|
|
|
34
34
|
- [Usage](#usage)
|
|
35
35
|
- [Roadmap](#roadmap)
|
|
36
36
|
- [Documentation](#documentation)
|
|
37
|
-
- [Reference
|
|
37
|
+
- [Reference Examples](#reference-examples)
|
|
38
38
|
|
|
39
39
|
## Who This Is For
|
|
40
40
|
|
|
@@ -105,28 +105,28 @@ For detailed documentation on each command, see:
|
|
|
105
105
|
**Create a new webapp:**
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
npx lousy-agents init --kind webapp
|
|
108
|
+
npx @lousy-agents/cli init --kind webapp
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
**Create a custom Copilot agent:**
|
|
112
112
|
|
|
113
113
|
```bash
|
|
114
|
-
npx lousy-agents new --copilot-agent security
|
|
114
|
+
npx @lousy-agents/cli new --copilot-agent security
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
**Generate Copilot setup workflow:**
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
npx lousy-agents copilot-setup
|
|
120
|
+
npx @lousy-agents/cli copilot-setup
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
## Roadmap
|
|
124
124
|
|
|
125
125
|
| Feature | Status |
|
|
126
126
|
|---------|--------|
|
|
127
|
-
| Scaffolding for webapps |
|
|
127
|
+
| Scaffolding for webapps | ✅ Complete |
|
|
128
|
+
| Scaffolding for REST APIs | ✅ Complete |
|
|
128
129
|
| Scaffolding for CLI | Not Started |
|
|
129
|
-
| Scaffolding for REST APIs | Not Started |
|
|
130
130
|
| Scaffolding for GraphQL APIs | Not Started |
|
|
131
131
|
| MCP server package | ✅ Complete |
|
|
132
132
|
|
|
@@ -137,12 +137,11 @@ npx lousy-agents copilot-setup
|
|
|
137
137
|
- **[`copilot-setup` Command](docs/copilot-setup.md)** - Workflow generation
|
|
138
138
|
- **[MCP Server](docs/mcp-server.md)** - AI assistant integration
|
|
139
139
|
|
|
140
|
-
## Reference
|
|
140
|
+
## Reference Examples
|
|
141
141
|
|
|
142
|
-
The
|
|
142
|
+
The repository includes fully working reference implementations demonstrating these patterns in action:
|
|
143
143
|
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
- Dev Container configuration for GitHub Codespaces
|
|
144
|
+
- **[ui/copilot-with-react](ui/copilot-with-react)** - Next.js + TypeScript webapp with pre-configured testing (Vitest), linting (Biome), GitHub Copilot instructions, and Dev Container configuration.
|
|
145
|
+
- **[api/copilot-with-fastify](api/copilot-with-fastify)** - Fastify + TypeScript REST API with Kysely, PostgreSQL, Testcontainers integration testing, and Dev Container configuration.
|
|
147
146
|
|
|
148
|
-
Launch a GitHub Codespace to instantly spin up
|
|
147
|
+
Launch a GitHub Codespace to instantly spin up either environment and experiment with spec-driven development.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= it.projectName %>",
|
|
3
|
+
"image": "mcr.microsoft.com/devcontainers/javascript-node:24-bookworm",
|
|
4
|
+
"features": {
|
|
5
|
+
"ghcr.io/devcontainers/features/github-cli:1": {
|
|
6
|
+
"version": "2.83.2"
|
|
7
|
+
},
|
|
8
|
+
"ghcr.io/rocker-org/devcontainer-features/apt-packages:1": {
|
|
9
|
+
"packages": "yamllint, shellcheck",
|
|
10
|
+
"upgradePackages": true
|
|
11
|
+
},
|
|
12
|
+
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
|
|
13
|
+
"version": "27.5.1"
|
|
14
|
+
},
|
|
15
|
+
"ghcr.io/devcontainers-extra/features/actionlint:1": {
|
|
16
|
+
"version": "1.7.9"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"forwardPorts": [3000, 5432],
|
|
20
|
+
"portsAttributes": {
|
|
21
|
+
"3000": {
|
|
22
|
+
"label": "API Server",
|
|
23
|
+
"onAutoForward": "notify"
|
|
24
|
+
},
|
|
25
|
+
"5432": {
|
|
26
|
+
"label": "PostgreSQL",
|
|
27
|
+
"onAutoForward": "silent"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"postCreateCommand": "npm cache add @upstash/context7-mcp @modelcontextprotocol/server-sequential-thinking",
|
|
31
|
+
"waitFor": "postCreateCommand",
|
|
32
|
+
"remoteUser": "node",
|
|
33
|
+
"customizations": {
|
|
34
|
+
"vscode": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"biomejs.biome",
|
|
37
|
+
"editorconfig.editorconfig",
|
|
38
|
+
"vitest.explorer",
|
|
39
|
+
"ms-vscode-remote.remote-containers",
|
|
40
|
+
"GitHub.codespaces",
|
|
41
|
+
"GitHub.Copilot",
|
|
42
|
+
"GitHub.Copilot-Chat",
|
|
43
|
+
"GitHub.github-vscode-theme",
|
|
44
|
+
"redhat.vscode-yaml",
|
|
45
|
+
"ms-azuretools.vscode-docker"
|
|
46
|
+
],
|
|
47
|
+
"settings": {
|
|
48
|
+
"editor.formatOnSave": true,
|
|
49
|
+
"editor.defaultFormatter": "biomejs.biome",
|
|
50
|
+
"[json]": {
|
|
51
|
+
"editor.defaultFormatter": "biomejs.biome"
|
|
52
|
+
},
|
|
53
|
+
"[javascript]": {
|
|
54
|
+
"editor.defaultFormatter": "biomejs.biome"
|
|
55
|
+
},
|
|
56
|
+
"[typescript]": {
|
|
57
|
+
"editor.defaultFormatter": "biomejs.biome"
|
|
58
|
+
},
|
|
59
|
+
"[yaml]": {
|
|
60
|
+
"editor.defaultFormatter": "redhat.vscode-yaml",
|
|
61
|
+
"editor.tabSize": 2
|
|
62
|
+
},
|
|
63
|
+
"vitest.enable": true,
|
|
64
|
+
"vitest.commandLine": "vitest",
|
|
65
|
+
"github.copilot.chat.agent.model": "claude-sonnet-4.5",
|
|
66
|
+
"github.copilot.chat.ask.model": "claude-sonnet-4.5",
|
|
67
|
+
"yaml.schemas": {
|
|
68
|
+
"https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml"
|
|
69
|
+
},
|
|
70
|
+
"github.copilot.chat.mcp.enabled": true
|
|
71
|
+
},
|
|
72
|
+
"mcp": {
|
|
73
|
+
"servers": {
|
|
74
|
+
"context7": {
|
|
75
|
+
"type": "stdio",
|
|
76
|
+
"command": "npx",
|
|
77
|
+
"args": ["-y", "@upstash/context7-mcp"]
|
|
78
|
+
},
|
|
79
|
+
"sequential-thinking": {
|
|
80
|
+
"command": "npx",
|
|
81
|
+
"args": [
|
|
82
|
+
"-y",
|
|
83
|
+
"@modelcontextprotocol/server-sequential-thinking"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
charset = utf-8
|
|
5
|
+
end_of_line = lf
|
|
6
|
+
indent_size = 4
|
|
7
|
+
indent_style = space
|
|
8
|
+
insert_final_newline = true
|
|
9
|
+
max_line_length = off
|
|
10
|
+
trim_trailing_whitespace = true
|
|
11
|
+
|
|
12
|
+
[{*.json}]
|
|
13
|
+
indent_size = 4
|
|
14
|
+
|
|
15
|
+
[{*.yaml,*.yml}]
|
|
16
|
+
indent_size = 2
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Copilot Feature To Spec
|
|
3
|
+
description: Create a spec for the Copilot coding agent.
|
|
4
|
+
title: "[Spec]: "
|
|
5
|
+
labels: ["copilot-ready", "enhancement"]
|
|
6
|
+
body:
|
|
7
|
+
- type: markdown
|
|
8
|
+
attributes:
|
|
9
|
+
value: |
|
|
10
|
+
## Copilot Specification
|
|
11
|
+
Please fill out the details below.
|
|
12
|
+
This structure is parsed by the Copilot Agent.
|
|
13
|
+
|
|
14
|
+
- type: textarea
|
|
15
|
+
id: context
|
|
16
|
+
attributes:
|
|
17
|
+
label: Context & Goal
|
|
18
|
+
description: What is the goal? What part of the codebase?
|
|
19
|
+
placeholder: |
|
|
20
|
+
We need to add a rate-limiting middleware to the API
|
|
21
|
+
gateway...
|
|
22
|
+
validations:
|
|
23
|
+
required: true
|
|
24
|
+
|
|
25
|
+
- type: textarea
|
|
26
|
+
id: acceptance-criteria
|
|
27
|
+
attributes:
|
|
28
|
+
label: Acceptance Criteria
|
|
29
|
+
description: How will we verify this is done?
|
|
30
|
+
placeholder: |
|
|
31
|
+
I run `npm run dev`.
|
|
32
|
+
I call `POST /api/users` with valid data.
|
|
33
|
+
I see a 201 response with the created user.
|
|
34
|
+
validations:
|
|
35
|
+
required: true
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------
|
|
38
|
+
# This section triggers the GitHub Script integration
|
|
39
|
+
# ---------------------------------------------------------
|
|
40
|
+
- type: textarea
|
|
41
|
+
id: extra-instructions
|
|
42
|
+
attributes:
|
|
43
|
+
label: Extra Instructions
|
|
44
|
+
description: Specific prompt engineering for the agent. This will be posted as a comment to guide the bot.
|
|
45
|
+
value: |
|
|
46
|
+
1. Review .github/instructions/spec.instructions.md for more details and use this as your guide for the feature specification.
|
|
47
|
+
2. Ensure you review all other repository instructions before starting work to understand engineering requirements.
|
|
48
|
+
3. Create your final spec output in the .github/specs directory and ensure requirements are captured in the EARS format.
|
|
49
|
+
4. Create a data flow diagram and sequence diagram to illustrate the feature's operation.
|
|
50
|
+
5. Break down the feature into manageable tasks with clear descriptions.
|
|
51
|
+
6. As you complete each task in the spec, mark the checkboxes as complete in the spec file using [x] notation.
|
|
52
|
+
7. If you need to make assumptions, ask questions on the issue.
|
|
53
|
+
8. Use the diagrams to inform your implementation plan.
|
|
54
|
+
validations:
|
|
55
|
+
required: false
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Fastify REST API Application
|
|
6
|
+
|
|
7
|
+
A Fastify TypeScript REST API following Test-Driven Development, Clean Architecture, and strict validation workflows with PostgreSQL database access via Kysely.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
Run `nvm use` before any npm command. During development, use file-scoped commands for faster feedback, and run the full validation suite (`npx biome check && npm test && npm run build`) before commits.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# ALWAYS run first
|
|
15
|
+
nvm use
|
|
16
|
+
|
|
17
|
+
# Core commands
|
|
18
|
+
npm install # Install deps (updates package-lock.json)
|
|
19
|
+
npm test # Run unit tests (vitest)
|
|
20
|
+
npm run test:integration # Run integration tests with Testcontainers
|
|
21
|
+
npm run build # Production build
|
|
22
|
+
npm run dev # Start development server with hot reload
|
|
23
|
+
npx biome check # Lint check
|
|
24
|
+
npx biome check --write # Auto-fix lint/format
|
|
25
|
+
|
|
26
|
+
# File-scoped (faster feedback)
|
|
27
|
+
npx biome check path/to/file.ts
|
|
28
|
+
npm test path/to/file.test.ts
|
|
29
|
+
|
|
30
|
+
# Validation suite (run before commits)
|
|
31
|
+
npx biome check && npm test && npm run build
|
|
32
|
+
|
|
33
|
+
# Other
|
|
34
|
+
npm audit # Security check
|
|
35
|
+
npm run lint:workflows # Validate GitHub Actions (actionlint)
|
|
36
|
+
npm run lint:yaml # Validate YAML (yamllint)
|
|
37
|
+
npm run db:migrate # Run database migrations
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Workflow: TDD Required
|
|
41
|
+
|
|
42
|
+
Follow this exact sequence for ALL code changes. Work in small increments — make one change at a time and validate before proceeding.
|
|
43
|
+
|
|
44
|
+
1. **Research**: Search codebase for existing patterns, routes, utilities. Use Context7 MCP tools for library/API documentation.
|
|
45
|
+
2. **Write failing test**: Create test describing desired behavior
|
|
46
|
+
3. **Verify failure**: Run `npm test` — confirm clear failure message
|
|
47
|
+
4. **Implement minimal code**: Write just enough to pass
|
|
48
|
+
5. **Verify pass**: Run `npm test` — confirm pass
|
|
49
|
+
6. **Refactor**: Clean up, remove duplication, keep tests green
|
|
50
|
+
7. **Validate**: `npx biome check && npm test && npm run build`
|
|
51
|
+
|
|
52
|
+
Task is NOT complete until all validation passes.
|
|
53
|
+
|
|
54
|
+
## Tech Stack
|
|
55
|
+
|
|
56
|
+
- **Framework**: Fastify — high performance, extensible Node.js web framework
|
|
57
|
+
- **Language**: TypeScript (strict mode)
|
|
58
|
+
- **Database**: PostgreSQL with Kysely (type-safe SQL query builder) + Postgres.js (driver)
|
|
59
|
+
- **Validation**: Zod for runtime validation of request/response data
|
|
60
|
+
- **Testing**: Vitest (never Jest), Testcontainers for database integration tests, Chance.js for test fixtures
|
|
61
|
+
- **Linting**: Biome (never ESLint/Prettier separately)
|
|
62
|
+
- **Logging**: Pino with JSON format and child loggers
|
|
63
|
+
- **HTTP**: fetch API only (for external service calls)
|
|
64
|
+
- **Architecture**: Clean Architecture principles
|
|
65
|
+
|
|
66
|
+
## Project Structure
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
.github/ GitHub Actions workflows
|
|
70
|
+
src/ Application source code
|
|
71
|
+
entities/ Layer 1: Business domain entities
|
|
72
|
+
use-cases/ Layer 2: Application business rules
|
|
73
|
+
gateways/ Layer 3: Database and external service adapters
|
|
74
|
+
routes/ Layer 3: Fastify route handlers
|
|
75
|
+
plugins/ Fastify plugins (auth, validation, etc.)
|
|
76
|
+
db/ Database configuration and migrations
|
|
77
|
+
lib/ Utilities and helpers
|
|
78
|
+
index.ts Application entry point
|
|
79
|
+
tests/ Test files (mirror src/ structure)
|
|
80
|
+
.nvmrc Node.js version (latest LTS)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Code Style
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { z } from 'zod';
|
|
87
|
+
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
88
|
+
|
|
89
|
+
// Define schema for runtime validation
|
|
90
|
+
const UserSchema = z.object({
|
|
91
|
+
id: z.string().uuid(),
|
|
92
|
+
name: z.string().min(1),
|
|
93
|
+
email: z.string().email(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
type User = z.infer<typeof UserSchema>;
|
|
97
|
+
|
|
98
|
+
// ✅ Good - small, typed, single purpose, descriptive names, runtime validation
|
|
99
|
+
async function getUserById(
|
|
100
|
+
request: FastifyRequest<{ Params: { id: string } }>,
|
|
101
|
+
reply: FastifyReply
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const { id } = request.params;
|
|
104
|
+
|
|
105
|
+
if (!id) {
|
|
106
|
+
return reply.badRequest('User ID required');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const user = await userRepository.findById(id);
|
|
110
|
+
|
|
111
|
+
if (!user) {
|
|
112
|
+
return reply.notFound(`User ${id} not found`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return reply.send(UserSchema.parse(user));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ❌ Bad - untyped, no validation, multiple responsibilities, no error handling
|
|
119
|
+
async function doStuff(req, reply) {
|
|
120
|
+
console.log('getting user');
|
|
121
|
+
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
|
122
|
+
return user;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Rules:**
|
|
127
|
+
- Always use TypeScript type hints
|
|
128
|
+
- Use descriptive names for variables, functions, and modules
|
|
129
|
+
- Functions must be small and have single responsibility
|
|
130
|
+
- Avoid god functions and classes — break into smaller, focused units
|
|
131
|
+
- Avoid repetitive code — extract reusable functions
|
|
132
|
+
- Extract functions when there are multiple code paths
|
|
133
|
+
- Favor immutability and pure functions
|
|
134
|
+
- Avoid temporal coupling
|
|
135
|
+
- Keep cyclomatic complexity low
|
|
136
|
+
- Remove all unused imports and variables
|
|
137
|
+
- Validate external data at runtime with Zod — never use type assertions (`as Type`) on API responses
|
|
138
|
+
- Use Fastify's built-in reply methods for error responses
|
|
139
|
+
- Run lint and tests after EVERY change
|
|
140
|
+
|
|
141
|
+
## Database Access with Kysely
|
|
142
|
+
|
|
143
|
+
Use Kysely for type-safe database queries. Never use raw SQL strings without Kysely's query builder.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
147
|
+
import postgres from 'postgres';
|
|
148
|
+
|
|
149
|
+
// Define database schema types
|
|
150
|
+
interface Database {
|
|
151
|
+
users: UserTable;
|
|
152
|
+
posts: PostTable;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface UserTable {
|
|
156
|
+
id: string;
|
|
157
|
+
name: string;
|
|
158
|
+
email: string;
|
|
159
|
+
created_at: Date;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Create Kysely instance
|
|
163
|
+
const sql = postgres({
|
|
164
|
+
host: process.env.DB_HOST,
|
|
165
|
+
port: Number(process.env.DB_PORT),
|
|
166
|
+
database: process.env.DB_NAME,
|
|
167
|
+
username: process.env.DB_USER,
|
|
168
|
+
password: process.env.DB_PASSWORD,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const db = new Kysely<Database>({
|
|
172
|
+
dialect: new PostgresDialect({ pool: sql }),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ✅ Good - type-safe queries
|
|
176
|
+
async function findUserById(id: string): Promise<User | undefined> {
|
|
177
|
+
return db
|
|
178
|
+
.selectFrom('users')
|
|
179
|
+
.select(['id', 'name', 'email'])
|
|
180
|
+
.where('id', '=', id)
|
|
181
|
+
.executeTakeFirst();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ✅ Good - type-safe inserts
|
|
185
|
+
async function createUser(user: Omit<User, 'id'>): Promise<User> {
|
|
186
|
+
return db
|
|
187
|
+
.insertInto('users')
|
|
188
|
+
.values({ ...user, id: crypto.randomUUID() })
|
|
189
|
+
.returningAll()
|
|
190
|
+
.executeTakeFirstOrThrow();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ❌ Bad - raw SQL without type safety (SECURITY RISK: SQL injection vulnerability)
|
|
194
|
+
async function findUser(id: string) {
|
|
195
|
+
// NEVER use string interpolation in SQL - this is vulnerable to SQL injection attacks!
|
|
196
|
+
return db.raw(`SELECT * FROM users WHERE id = '${id}'`);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Testing Standards
|
|
201
|
+
|
|
202
|
+
Tests are executable documentation. Use Arrange-Act-Assert pattern. Use Testcontainers for database integration tests. Generate test fixtures with Chance.js.
|
|
203
|
+
|
|
204
|
+
### Unit Tests
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import Chance from 'chance';
|
|
208
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
209
|
+
import { createUserUseCase } from './create-user';
|
|
210
|
+
|
|
211
|
+
const chance = new Chance();
|
|
212
|
+
|
|
213
|
+
// ✅ Good - describes behavior, uses generated fixtures, mocks repository
|
|
214
|
+
describe('Create User Use Case', () => {
|
|
215
|
+
describe('given valid user data', () => {
|
|
216
|
+
it('creates the user and returns it', async () => {
|
|
217
|
+
// Arrange
|
|
218
|
+
const userData = {
|
|
219
|
+
name: chance.name(),
|
|
220
|
+
email: chance.email(),
|
|
221
|
+
};
|
|
222
|
+
const expectedUser = { id: chance.guid(), ...userData };
|
|
223
|
+
const mockRepository = {
|
|
224
|
+
create: vi.fn().mockResolvedValue(expectedUser),
|
|
225
|
+
findByEmail: vi.fn().mockResolvedValue(null),
|
|
226
|
+
};
|
|
227
|
+
const useCase = createUserUseCase(mockRepository);
|
|
228
|
+
|
|
229
|
+
// Act
|
|
230
|
+
const result = await useCase.execute(userData);
|
|
231
|
+
|
|
232
|
+
// Assert
|
|
233
|
+
expect(result).toEqual(expectedUser);
|
|
234
|
+
expect(mockRepository.create).toHaveBeenCalledWith(userData);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('given an email that already exists', () => {
|
|
239
|
+
it('throws a conflict error', async () => {
|
|
240
|
+
// Arrange
|
|
241
|
+
const existingEmail = chance.email();
|
|
242
|
+
const userData = { name: chance.name(), email: existingEmail };
|
|
243
|
+
const mockRepository = {
|
|
244
|
+
create: vi.fn(),
|
|
245
|
+
findByEmail: vi.fn().mockResolvedValue({ id: chance.guid(), ...userData }),
|
|
246
|
+
};
|
|
247
|
+
const useCase = createUserUseCase(mockRepository);
|
|
248
|
+
|
|
249
|
+
// Act & Assert
|
|
250
|
+
await expect(useCase.execute(userData)).rejects.toThrow('Email already exists');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Integration Tests with Testcontainers
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
|
|
260
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
261
|
+
import postgres from 'postgres';
|
|
262
|
+
import { afterAll, beforeAll, describe, it, expect } from 'vitest';
|
|
263
|
+
import { UserRepository } from './user-repository';
|
|
264
|
+
|
|
265
|
+
describe('User Repository Integration', () => {
|
|
266
|
+
let container: StartedPostgreSqlContainer;
|
|
267
|
+
let db: Kysely<Database>;
|
|
268
|
+
let repository: UserRepository;
|
|
269
|
+
|
|
270
|
+
beforeAll(async () => {
|
|
271
|
+
// Start PostgreSQL container
|
|
272
|
+
container = await new PostgreSqlContainer()
|
|
273
|
+
.withDatabase('test_db')
|
|
274
|
+
.start();
|
|
275
|
+
|
|
276
|
+
// Create database connection
|
|
277
|
+
const sql = postgres({
|
|
278
|
+
host: container.getHost(),
|
|
279
|
+
port: container.getPort(),
|
|
280
|
+
database: container.getDatabase(),
|
|
281
|
+
username: container.getUsername(),
|
|
282
|
+
password: container.getPassword(),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
db = new Kysely<Database>({
|
|
286
|
+
dialect: new PostgresDialect({ pool: sql }),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Run migrations
|
|
290
|
+
await runMigrations(db);
|
|
291
|
+
|
|
292
|
+
repository = new UserRepository(db);
|
|
293
|
+
}, 60000);
|
|
294
|
+
|
|
295
|
+
afterAll(async () => {
|
|
296
|
+
await db.destroy();
|
|
297
|
+
await container.stop();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('given a new user', () => {
|
|
301
|
+
it('persists the user to the database', async () => {
|
|
302
|
+
// Arrange
|
|
303
|
+
const userData = {
|
|
304
|
+
name: 'John Doe',
|
|
305
|
+
email: 'john@example.com',
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Act
|
|
309
|
+
const created = await repository.create(userData);
|
|
310
|
+
|
|
311
|
+
// Assert
|
|
312
|
+
expect(created.id).toBeDefined();
|
|
313
|
+
expect(created.name).toBe(userData.name);
|
|
314
|
+
expect(created.email).toBe(userData.email);
|
|
315
|
+
|
|
316
|
+
// Verify persistence
|
|
317
|
+
const found = await repository.findById(created.id);
|
|
318
|
+
expect(found).toEqual(created);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Rules:**
|
|
325
|
+
- Tests are executable documentation — describe behavior, not implementation
|
|
326
|
+
- Name `describe` blocks for features/scenarios, not function names
|
|
327
|
+
- Name `it` blocks as specifications that read as complete sentences
|
|
328
|
+
- Use nested `describe` blocks for "given/when" context
|
|
329
|
+
- Use Chance.js to generate test fixtures — avoid hardcoded test data
|
|
330
|
+
- Extract test data to constants — never duplicate values across arrange/act/assert
|
|
331
|
+
- Use Vitest (never Jest)
|
|
332
|
+
- Use Testcontainers for database integration tests
|
|
333
|
+
- Follow Arrange-Act-Assert pattern
|
|
334
|
+
- Tests must be deterministic — same result every run
|
|
335
|
+
- Avoid conditional logic in tests unless absolutely necessary
|
|
336
|
+
- Ensure all code paths have corresponding tests
|
|
337
|
+
- Test happy paths, unhappy paths, and edge cases
|
|
338
|
+
- Never modify tests to pass without understanding root cause
|
|
339
|
+
|
|
340
|
+
## Dependencies
|
|
341
|
+
|
|
342
|
+
- Use latest LTS Node.js — check with `nvm ls-remote --lts`, update `.nvmrc`
|
|
343
|
+
- Pin ALL dependencies to exact versions (no ^ or ~)
|
|
344
|
+
- Use explicit version numbers when adding new dependencies
|
|
345
|
+
- Search npm for latest stable version before adding
|
|
346
|
+
- Run `npm audit` after any dependency change
|
|
347
|
+
- Ensure `package-lock.json` is updated correctly
|
|
348
|
+
- Use Dependabot to keep dependencies current
|
|
349
|
+
|
|
350
|
+
## GitHub Actions
|
|
351
|
+
|
|
352
|
+
- Validation must be automated via GitHub Actions and runnable locally the same way
|
|
353
|
+
- Integration tests require Docker for Testcontainers
|
|
354
|
+
- Validate all workflows using actionlint
|
|
355
|
+
- Validate all YAML files using yamllint
|
|
356
|
+
- Pin all 3rd party Actions to specific version or commit SHA
|
|
357
|
+
- Keep all 3rd party Actions updated to latest version
|
|
358
|
+
|
|
359
|
+
## Boundaries
|
|
360
|
+
|
|
361
|
+
**✅ Always do:**
|
|
362
|
+
- Run `nvm use` before any npm command
|
|
363
|
+
- Write tests before implementation (TDD)
|
|
364
|
+
- Run lint and tests after every change
|
|
365
|
+
- Run full validation before commits
|
|
366
|
+
- Use existing patterns from codebase
|
|
367
|
+
- Work in small increments
|
|
368
|
+
- Use Kysely for all database queries
|
|
369
|
+
- Validate all request/response data with Zod
|
|
370
|
+
- Use Context7 MCP tools for code generation and documentation
|
|
371
|
+
|
|
372
|
+
**⚠️ Ask first:**
|
|
373
|
+
- Adding new dependencies
|
|
374
|
+
- Changing project structure
|
|
375
|
+
- Modifying GitHub Actions workflows
|
|
376
|
+
- Database schema changes
|
|
377
|
+
- Adding new database tables
|
|
378
|
+
|
|
379
|
+
**🚫 Never do:**
|
|
380
|
+
- Skip the TDD workflow
|
|
381
|
+
- Store secrets in code (use environment variables)
|
|
382
|
+
- Use Jest (use Vitest)
|
|
383
|
+
- Use raw SQL strings (use Kysely query builder)
|
|
384
|
+
- Modify tests to pass without fixing root cause
|
|
385
|
+
- Add dependencies without explicit version numbers
|
|
386
|
+
- Use type assertions (`as Type`) on external/API data
|
|
387
|
+
- Use Prisma or other ORMs (use Kysely)
|