@quanticjs/create-app 0.1.1
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/deps.d.ts +4 -0
- package/dist/deps.js +97 -0
- package/dist/deps.js.map +1 -0
- package/dist/generators/backend.d.ts +2 -0
- package/dist/generators/backend.js +20 -0
- package/dist/generators/backend.js.map +1 -0
- package/dist/generators/bff.d.ts +2 -0
- package/dist/generators/bff.js +9 -0
- package/dist/generators/bff.js.map +1 -0
- package/dist/generators/claude.d.ts +2 -0
- package/dist/generators/claude.js +45 -0
- package/dist/generators/claude.js.map +1 -0
- package/dist/generators/docker.d.ts +2 -0
- package/dist/generators/docker.js +10 -0
- package/dist/generators/docker.js.map +1 -0
- package/dist/generators/e2e.d.ts +2 -0
- package/dist/generators/e2e.js +8 -0
- package/dist/generators/e2e.js.map +1 -0
- package/dist/generators/frontend.d.ts +2 -0
- package/dist/generators/frontend.js +28 -0
- package/dist/generators/frontend.js.map +1 -0
- package/dist/generators/module.d.ts +2 -0
- package/dist/generators/module.js +35 -0
- package/dist/generators/module.js.map +1 -0
- package/dist/generators/root.d.ts +2 -0
- package/dist/generators/root.js +7 -0
- package/dist/generators/root.js.map +1 -0
- package/dist/generators/scripts.d.ts +2 -0
- package/dist/generators/scripts.js +10 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +8 -0
- package/dist/prompts.js +53 -0
- package/dist/prompts.js.map +1 -0
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.js +79 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/utils/exec.d.ts +2 -0
- package/dist/utils/exec.js +14 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/utils/template.d.ts +10 -0
- package/dist/utils/template.js +20 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validate.d.ts +4 -0
- package/dist/utils/validate.js +27 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +50 -0
- package/templates/backend/app.module.ts.ejs +61 -0
- package/templates/backend/bff.controller.ts.ejs +60 -0
- package/templates/backend/bff.module.ts.ejs +10 -0
- package/templates/backend/bff.service.ts.ejs +6 -0
- package/templates/backend/create-schema.migration.ts.ejs +15 -0
- package/templates/backend/data-source.ts.ejs +13 -0
- package/templates/backend/env.example.ejs +30 -0
- package/templates/backend/main.ts.ejs +43 -0
- package/templates/backend/module.ts.ejs +14 -0
- package/templates/backend/nest-cli.json.ejs +8 -0
- package/templates/backend/package.json.ejs +23 -0
- package/templates/backend/tsconfig.build.json.ejs +4 -0
- package/templates/backend/tsconfig.json.ejs +24 -0
- package/templates/claude/CLAUDE.md.ejs +86 -0
- package/templates/claude/hooks/auto-format.sh +22 -0
- package/templates/claude/hooks/check-secrets.sh +49 -0
- package/templates/claude/hooks/guard-destructive.sh +42 -0
- package/templates/claude/hooks/on-compaction.sh +29 -0
- package/templates/claude/mcp.json +10 -0
- package/templates/claude/rules/api-patterns.md +86 -0
- package/templates/claude/rules/auth-patterns.md +109 -0
- package/templates/claude/rules/backend-patterns.md +421 -0
- package/templates/claude/rules/database-patterns.md +96 -0
- package/templates/claude/rules/docker-patterns.md +86 -0
- package/templates/claude/rules/frontend-patterns.md +262 -0
- package/templates/claude/rules/observability-backend.md +132 -0
- package/templates/claude/rules/observability-frontend.md +49 -0
- package/templates/claude/rules/playwright-mcp.md +80 -0
- package/templates/claude/rules/resilience-ops.md +103 -0
- package/templates/claude/rules/testing-e2e-ui.md +190 -0
- package/templates/claude/rules/testing-patterns.md +94 -0
- package/templates/claude/rules/workflow-backend.md +64 -0
- package/templates/claude/rules/workflow-frontend.md +60 -0
- package/templates/claude/settings.json +68 -0
- package/templates/claude/skills/add-api-endpoint/SKILL.md +59 -0
- package/templates/claude/skills/add-auth-endpoint/SKILL.md +68 -0
- package/templates/claude/skills/add-entity/SKILL.md +56 -0
- package/templates/claude/skills/add-event/SKILL.md +127 -0
- package/templates/claude/skills/add-feature/SKILL.md +20 -0
- package/templates/claude/skills/add-frontend-page/SKILL.md +75 -0
- package/templates/claude/skills/add-handler/SKILL.md +105 -0
- package/templates/claude/skills/add-integration/SKILL.md +176 -0
- package/templates/claude/skills/add-migration/SKILL.md +20 -0
- package/templates/claude/skills/add-module/SKILL.md +89 -0
- package/templates/claude/skills/add-realtime/SKILL.md +119 -0
- package/templates/claude/skills/audit-rules/SKILL.md +120 -0
- package/templates/claude/skills/debugging/SKILL.md +105 -0
- package/templates/claude/skills/docker-dev/SKILL.md +86 -0
- package/templates/claude/skills/e2e-audit/SKILL.md +85 -0
- package/templates/claude/skills/e2e-full/SKILL.md +132 -0
- package/templates/claude/skills/e2e-scan/SKILL.md +171 -0
- package/templates/claude/skills/e2e-verify/SKILL.md +145 -0
- package/templates/claude/skills/fix-bug/SKILL.md +33 -0
- package/templates/claude/skills/implement-spec/SKILL.md +98 -0
- package/templates/claude/skills/review-code/SKILL.md +109 -0
- package/templates/claude/skills/review-spec/SKILL.md +216 -0
- package/templates/claude/skills/run-tests/SKILL.md +37 -0
- package/templates/claude/skills/specify/SKILL.md +87 -0
- package/templates/claude/skills/write-backend-tests/SKILL.md +182 -0
- package/templates/claude/skills/write-ui-tests/SKILL.md +118 -0
- package/templates/docker/Dockerfile.client.ejs +14 -0
- package/templates/docker/Dockerfile.ejs +28 -0
- package/templates/docker/docker-compose.test.yml.ejs +54 -0
- package/templates/docker/docker-compose.yml.ejs +76 -0
- package/templates/docker/nginx.conf.ejs +21 -0
- package/templates/frontend/App.tsx.ejs +64 -0
- package/templates/frontend/DashboardPage.tsx.ejs +37 -0
- package/templates/frontend/LoginPage.tsx.ejs +20 -0
- package/templates/frontend/NotFoundPage.tsx.ejs +15 -0
- package/templates/frontend/api-client.ts.ejs +15 -0
- package/templates/frontend/index.css.ejs +57 -0
- package/templates/frontend/index.html.ejs +13 -0
- package/templates/frontend/main.tsx.ejs +10 -0
- package/templates/frontend/package.json.ejs +16 -0
- package/templates/frontend/playwright.config.ts.ejs +20 -0
- package/templates/frontend/postcss.config.js.ejs +3 -0
- package/templates/frontend/smoke.spec.ts.ejs +37 -0
- package/templates/frontend/tailwind.config.ts.ejs +56 -0
- package/templates/frontend/tsconfig.json.ejs +25 -0
- package/templates/frontend/tsconfig.node.json.ejs +15 -0
- package/templates/frontend/utils.ts.ejs +6 -0
- package/templates/frontend/vite-env.d.ts.ejs +1 -0
- package/templates/frontend/vite.config.ts.ejs +20 -0
- package/templates/root/gitignore.ejs +9 -0
- package/templates/root/prettierrc.ejs +7 -0
- package/templates/scripts/init-db.sh.ejs +8 -0
- package/templates/scripts/save-auth-state.ts.ejs +24 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Playwright MCP — Browser Control for Claude Code
|
|
2
|
+
|
|
3
|
+
You have a Playwright MCP server configured (`.claude/mcp.json`). This gives you **real-time browser control** — navigate pages, click elements, fill forms, and take screenshots without writing test files.
|
|
4
|
+
|
|
5
|
+
**State "using Playwright MCP" in your first message of an MCP session** — otherwise Claude may default to Bash-driven Playwright commands instead of the MCP tools.
|
|
6
|
+
|
|
7
|
+
## Setup & Version Pinning
|
|
8
|
+
|
|
9
|
+
- **Package:** Microsoft's `@playwright/mcp` — **NOT** the community `@executeautomation/playwright-mcp-server`. Do not confuse them.
|
|
10
|
+
- **Pin the version** in `.claude/mcp.json`. Never use `@latest` in shared configs — beta releases silently break tool schemas mid-session.
|
|
11
|
+
- **Node 18+ required.** Older versions throw `performance is not defined`. Verify with `node --version` before any MCP run.
|
|
12
|
+
- **Install browser binaries** after any version bump: `npx playwright install` (and `npx playwright install-deps` on Linux/CI).
|
|
13
|
+
- **Smoke test** after setup: `browser_navigate` to `http://localhost:5173/` and confirm the a11y tree returns.
|
|
14
|
+
|
|
15
|
+
## CI / Headless
|
|
16
|
+
|
|
17
|
+
- Pass `--headless` to the MCP server args when running in CI or Docker — default is headed.
|
|
18
|
+
- For repeatable CI runs, prefer coded specs (`client/e2e/`) over MCP. Use MCP for exploration and debugging, not pipeline execution.
|
|
19
|
+
|
|
20
|
+
## Two Uses
|
|
21
|
+
|
|
22
|
+
### 1. Visual Self-QA (during implementation)
|
|
23
|
+
After implementing UI changes, open the page and verify it renders correctly:
|
|
24
|
+
- `browser_navigate` to the page → `browser_snapshot` (content) or `browser_screenshot` (visual layout) → check it looks right
|
|
25
|
+
- Use during any issue that touches `.tsx` files
|
|
26
|
+
|
|
27
|
+
### 2. E2E Journey Verification (via `/e2e-verify`)
|
|
28
|
+
Walk through full user journeys by browsing the app interactively. This replaces hardcoded Playwright spec files for UI verification. See `.claude/skills/e2e-verify/SKILL.md` for the journey definitions.
|
|
29
|
+
|
|
30
|
+
## MCP Tools
|
|
31
|
+
|
|
32
|
+
- `browser_navigate` — open a URL
|
|
33
|
+
- `browser_click` — click an element
|
|
34
|
+
- `browser_fill` — type into an input
|
|
35
|
+
- `browser_snapshot` — return the accessibility tree (~120 tokens, stable selectors)
|
|
36
|
+
- `browser_screenshot` — capture a visual screenshot (~1,500 tokens)
|
|
37
|
+
- `browser_select_option`, `browser_hover`, `browser_drag`, etc.
|
|
38
|
+
|
|
39
|
+
## Snapshot vs Screenshot
|
|
40
|
+
|
|
41
|
+
Prefer `browser_snapshot` for element discovery, interaction, and content verification — it returns the accessibility tree, costs ~12x fewer tokens than a screenshot, and provides more stable element references.
|
|
42
|
+
|
|
43
|
+
Use `browser_take_screenshot` only when verifying **visual appearance** (colors, layout, spacing, images).
|
|
44
|
+
|
|
45
|
+
## Authenticated Pages
|
|
46
|
+
|
|
47
|
+
Most pages require login. The app uses BFF httpOnly cookies — not sessionStorage tokens.
|
|
48
|
+
|
|
49
|
+
Run `cd client && npx tsx ../scripts/save-auth-state.ts` first (requires Docker stack up). This completes the BFF OIDC login flow through Keycloak and saves the resulting httpOnly session cookie to `client/e2e/auth/storage-state.json`.
|
|
50
|
+
|
|
51
|
+
If pages redirect to the login page, the storage state is expired — re-run the save script.
|
|
52
|
+
|
|
53
|
+
## Ports
|
|
54
|
+
|
|
55
|
+
### Dev Stack (visual self-QA during development)
|
|
56
|
+
|
|
57
|
+
| Service | URL | When |
|
|
58
|
+
|---------|-----|------|
|
|
59
|
+
| Vite dev server | `http://localhost:5173` | `cd client && npm run dev` |
|
|
60
|
+
| Backend API | `http://localhost:3000` | `docker compose up` |
|
|
61
|
+
| Keycloak | `http://localhost:8080` | `docker compose up` |
|
|
62
|
+
|
|
63
|
+
### Test Stack (E2E skills: `/e2e-full`, `/e2e-verify`, `/e2e-audit`)
|
|
64
|
+
|
|
65
|
+
| Service | URL | When |
|
|
66
|
+
|---------|-----|------|
|
|
67
|
+
| Vite (test) | `http://localhost:5199` | `cd client && VITE_API_URL=http://localhost:3099 npm run dev -- --port 5199` |
|
|
68
|
+
| Backend API (test) | `http://localhost:3099` | `docker compose -f docker-compose.test.yml up` |
|
|
69
|
+
| Keycloak (test) | `http://localhost:8099` | `docker compose -f docker-compose.test.yml up` |
|
|
70
|
+
|
|
71
|
+
**E2E tests MUST use the test stack** — never the dev stack.
|
|
72
|
+
|
|
73
|
+
## Do NOT
|
|
74
|
+
|
|
75
|
+
- Do not commit `client/e2e/auth/storage-state.json` — it contains httpOnly session cookies
|
|
76
|
+
- Do not use MCP for API contract assertions (status codes, JSON payloads) — use coded tests for those
|
|
77
|
+
- Do not use MCP as a replacement for the mocked UI E2E suite (`client/e2e/`) — MCP is for exploratory/visual verification only
|
|
78
|
+
- Do not point MCP at environments with real user data. Every page's DOM, console output, and form values get transmitted to Anthropic's API — keep MCP restricted to the local dev stack
|
|
79
|
+
- Do not use `@playwright/mcp@latest` in `.claude/mcp.json` — pin a specific version
|
|
80
|
+
- Do not install `@executeautomation/playwright-mcp-server` thinking it's the same package — it isn't
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: "src/**/*.ts"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Resilience & Operations Patterns
|
|
6
|
+
|
|
7
|
+
## Health Probes
|
|
8
|
+
|
|
9
|
+
Every service imports `QuanticHealthModule.forRoot()` in `app.module.ts`. It provides three Kubernetes probes:
|
|
10
|
+
|
|
11
|
+
| Probe | Path | Checks | Purpose |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| Liveness | `/health/live` | Event loop only | Is the process alive? Restart if not. |
|
|
14
|
+
| Readiness | `/health/ready` | DB + Redis (auto-detected) + custom | Can it serve traffic? Remove from LB if not. |
|
|
15
|
+
| Startup | `/health/startup` | User-configured | Has initialization completed? |
|
|
16
|
+
|
|
17
|
+
**Auto-detection:** If `DataSource` or `REDIS_CLIENT` is in the DI container, readiness checks are registered automatically. Disable with `autoDetect: false`.
|
|
18
|
+
|
|
19
|
+
**Transport modes:**
|
|
20
|
+
- `controller` (default) — mounts on existing NestJS server, routes are `@Public()`
|
|
21
|
+
- `standalone` — separate `http.createServer` on dedicated port (for workers/queue consumers)
|
|
22
|
+
- `file` — writes to `/tmp/.healthy` on interval (for cron jobs)
|
|
23
|
+
- `none` — programmatic access only via `HealthRegistry`
|
|
24
|
+
|
|
25
|
+
**Custom checks:**
|
|
26
|
+
```typescript
|
|
27
|
+
QuanticHealthModule.forRoot({
|
|
28
|
+
readiness: [
|
|
29
|
+
{ name: 'minio', check: () => minioClient.bucketExists('uploads'), timeoutMs: 5000 },
|
|
30
|
+
{ name: 'payments', url: 'http://payments:3000/health/live', timeoutMs: 3000 },
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Shutdown-aware:** On SIGTERM, readiness flips to 503 immediately, waits `shutdownDelayMs` (default 5s) for LB to stop routing, then `GracefulShutdownService` drains resources.
|
|
36
|
+
|
|
37
|
+
## Graceful Shutdown
|
|
38
|
+
|
|
39
|
+
On SIGTERM, shutdown runs in two phases:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
SIGTERM → Phase 1: readiness → 503, wait 5s (LB stops routing)
|
|
43
|
+
→ Phase 2: drainWork() → close DB → close Redis → exit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Services with custom resources (queues, websockets, outbox publisher) override `drainWork()`:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
@Injectable()
|
|
50
|
+
export class AppShutdownService extends GracefulShutdownService {
|
|
51
|
+
constructor(
|
|
52
|
+
@Optional() dataSource: DataSource,
|
|
53
|
+
@Optional() @Inject('REDIS_CLIENT') redis: Redis,
|
|
54
|
+
private readonly queueWorker: Worker,
|
|
55
|
+
) {
|
|
56
|
+
super(dataSource, redis);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected async drainWork(): Promise<void> {
|
|
60
|
+
await this.queueWorker.close(); // stop accepting jobs, wait for in-progress
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Kubernetes alignment:** `terminationGracePeriodSeconds` must exceed `shutdownDelayMs + drainTimeout + buffer` (default: 45s in Helm chart).
|
|
66
|
+
|
|
67
|
+
## Circuit Breaker
|
|
68
|
+
|
|
69
|
+
All outbound HTTP calls to external services must use `createCircuitBreaker()`:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { createCircuitBreaker } from '@quanticjs/core';
|
|
73
|
+
|
|
74
|
+
const policy = createCircuitBreaker({
|
|
75
|
+
maxRetries: 2, // 3 total attempts, exponential backoff
|
|
76
|
+
consecutiveFailures: 5, // open circuit after 5 consecutive failures
|
|
77
|
+
halfOpenAfterMs: 30_000, // test one request after 30s
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = await policy.execute(() => httpClient.get('/external-api'));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**States:** Closed (normal) → Open (fast-fail, no outbound calls) → Half-open (test one request) → Closed on success.
|
|
84
|
+
|
|
85
|
+
**Where to apply:**
|
|
86
|
+
|
|
87
|
+
| Integration | Circuit breaker? |
|
|
88
|
+
|---|---|
|
|
89
|
+
| Keycloak JWKS | Yes |
|
|
90
|
+
| Kogito workflow | Yes |
|
|
91
|
+
| Third-party APIs | Yes |
|
|
92
|
+
| Redis | No — ioredis has built-in retry |
|
|
93
|
+
| TypeORM / DB | No — connection pool retries internally |
|
|
94
|
+
|
|
95
|
+
## NEVER
|
|
96
|
+
|
|
97
|
+
- **NEVER** make liveness depend on external services (DB, Redis) — liveness checks the process only; dependency failures go in readiness
|
|
98
|
+
- **NEVER** skip `QuanticHealthModule.forRoot()` in `app.module.ts` — every service needs health probes
|
|
99
|
+
- **NEVER** exit on SIGTERM without draining — extend `GracefulShutdownService` and close custom resources in `drainWork()`
|
|
100
|
+
- **NEVER** make outbound HTTP calls to external services without a circuit breaker
|
|
101
|
+
- **NEVER** retry 4xx responses — they are deterministic client errors
|
|
102
|
+
- **NEVER** share a circuit breaker across integrations — one failing service must not trip the circuit for healthy ones
|
|
103
|
+
- **NEVER** wrap Redis or TypeORM calls in a circuit breaker — they have built-in retry/reconnect
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: "client/e2e/**/*.ts, client/playwright.config.ts"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Testing — E2E UI (Playwright, Mocked APIs)
|
|
6
|
+
|
|
7
|
+
All API calls are mocked with `page.route()`. Tests verify the UI renders correctly and user interactions work.
|
|
8
|
+
|
|
9
|
+
## Coverage Requirements (MANDATORY)
|
|
10
|
+
|
|
11
|
+
**Every spec file MUST test all four states:**
|
|
12
|
+
|
|
13
|
+
| State | What to test | How to mock |
|
|
14
|
+
|-------|-------------|-------------|
|
|
15
|
+
| **Happy path** | Feature works as expected | Mock API returns 200 with valid data |
|
|
16
|
+
| **Error state** | API failure shows error UI | Mock API returns 500, verify error message shown |
|
|
17
|
+
| **Empty state** | No data shows empty UI | Mock API returns 200 with empty array |
|
|
18
|
+
| **Loading state** | Skeleton/spinner shown while loading | Delay API response, verify skeleton visible |
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
test.describe('Projects List', () => {
|
|
22
|
+
test('shows project cards on success', async ({ page }) => {
|
|
23
|
+
await page.route('**/api/projects*', route => route.fulfill({
|
|
24
|
+
status: 200, json: { value: mockProjects },
|
|
25
|
+
}));
|
|
26
|
+
await page.goto('/projects');
|
|
27
|
+
await expect(page.getByText('My Project')).toBeVisible();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('shows error message on API failure', async ({ page }) => {
|
|
31
|
+
await page.route('**/api/projects*', route => route.fulfill({ status: 500 }));
|
|
32
|
+
await page.goto('/projects');
|
|
33
|
+
await expect(page.getByText(/something went wrong|error|try again/i)).toBeVisible();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('shows empty state when no projects', async ({ page }) => {
|
|
37
|
+
await page.route('**/api/projects*', route => route.fulfill({
|
|
38
|
+
status: 200, json: { value: [] },
|
|
39
|
+
}));
|
|
40
|
+
await page.goto('/projects');
|
|
41
|
+
await expect(page.getByText(/no projects/i)).toBeVisible();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('shows loading while fetching', async ({ page }) => {
|
|
45
|
+
await page.route('**/api/projects*', async route => {
|
|
46
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
47
|
+
await route.fulfill({ status: 200, json: { value: [] } });
|
|
48
|
+
});
|
|
49
|
+
await page.goto('/projects');
|
|
50
|
+
await expect(page.locator('[role="status"]')).toBeVisible();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Auth Mocking (MANDATORY)
|
|
56
|
+
|
|
57
|
+
Auth uses BFF httpOnly cookies — **not** localStorage/sessionStorage tokens.
|
|
58
|
+
|
|
59
|
+
Mock the BFF `/auth/me` endpoint in `beforeEach`:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
test.beforeEach(async ({ page }) => {
|
|
63
|
+
await page.route('**/auth/me', route => route.fulfill({
|
|
64
|
+
status: 200,
|
|
65
|
+
contentType: 'application/json',
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
id: 'user-1',
|
|
68
|
+
keycloakId: 'kc-1',
|
|
69
|
+
email: 'test@test.com',
|
|
70
|
+
displayName: 'Test User',
|
|
71
|
+
role: 'user',
|
|
72
|
+
roles: ['user'],
|
|
73
|
+
permissions: ['project.read', 'project.create'],
|
|
74
|
+
}),
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('unauthenticated user redirected to login', async ({ page }) => {
|
|
79
|
+
await page.route('**/auth/me', route => route.fulfill({ status: 401 }));
|
|
80
|
+
await page.goto('/projects');
|
|
81
|
+
await expect(page).toHaveURL(/\/login/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('non-admin cannot access admin pages', async ({ page }) => {
|
|
85
|
+
// Default mock has role: 'user' — admin routes should redirect
|
|
86
|
+
await page.goto('/admin/prompt-templates');
|
|
87
|
+
await expect(page).not.toHaveURL('/admin/prompt-templates');
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**NEVER** mock auth by injecting tokens into `localStorage` or `sessionStorage` — the app uses BFF httpOnly cookies and checks auth via `/auth/me`.
|
|
92
|
+
|
|
93
|
+
## Locators — Semantic ONLY
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// ❌ WRONG
|
|
97
|
+
page.locator('.btn-primary');
|
|
98
|
+
page.locator('[data-testid="submit"]');
|
|
99
|
+
page.locator('.fixed.inset-0');
|
|
100
|
+
page.locator('[class*="animate-pulse"]');
|
|
101
|
+
|
|
102
|
+
// ✅ CORRECT
|
|
103
|
+
page.getByRole('button', { name: 'Submit' });
|
|
104
|
+
page.getByRole('heading', { name: 'Projects' });
|
|
105
|
+
page.getByLabel('Email address');
|
|
106
|
+
page.getByText('Welcome back');
|
|
107
|
+
page.getByPlaceholder('Search...');
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Priority:** `getByRole` > `getByLabel` > `getByText` > `getByPlaceholder` > `getByTestId` (last resort)
|
|
111
|
+
|
|
112
|
+
Chain and filter to narrow scope:
|
|
113
|
+
```typescript
|
|
114
|
+
const card = page.getByRole('listitem').filter({ hasText: 'My Project' });
|
|
115
|
+
await card.getByRole('link', { name: 'View' }).click();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Assertions — Web-First Only
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// ❌ WRONG — checks once, no retry, FLAKY
|
|
122
|
+
expect(await page.getByText('welcome').isVisible()).toBe(true);
|
|
123
|
+
|
|
124
|
+
// ✅ CORRECT — retries until visible or timeout
|
|
125
|
+
await expect(page.getByText('welcome')).toBeVisible();
|
|
126
|
+
await expect(page.getByRole('heading')).toHaveText('Dashboard');
|
|
127
|
+
await expect(page).toHaveURL('/projects');
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Waiting — Correct Alternatives
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// ❌ WRONG
|
|
134
|
+
await page.waitForTimeout(500);
|
|
135
|
+
|
|
136
|
+
// ✅ CORRECT
|
|
137
|
+
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
|
|
138
|
+
await page.waitForURL('/projects');
|
|
139
|
+
await page.waitForResponse('**/api/projects');
|
|
140
|
+
await expect(page.getByRole('heading')).toHaveText('Success');
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Responsive Viewport Testing
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
test.describe('tablet', () => {
|
|
147
|
+
test.use({ viewport: { width: 768, height: 1024 } });
|
|
148
|
+
test('sidebar collapses on tablet', async ({ page }) => {
|
|
149
|
+
await page.goto('/');
|
|
150
|
+
// verify collapsed sidebar behavior
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test.describe('desktop', () => {
|
|
155
|
+
test.use({ viewport: { width: 1440, height: 900 } });
|
|
156
|
+
test('shows expanded sidebar', async ({ page }) => {
|
|
157
|
+
await page.goto('/');
|
|
158
|
+
await expect(page.getByRole('navigation')).toBeVisible();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Test File Structure
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
client/e2e/
|
|
167
|
+
├── auth/
|
|
168
|
+
│ └── storage-state.json # gitignored — MCP session cookies
|
|
169
|
+
├── login.spec.ts
|
|
170
|
+
├── dashboard.spec.ts
|
|
171
|
+
├── projects-list.spec.ts
|
|
172
|
+
├── create-project.spec.ts
|
|
173
|
+
├── project-detail.spec.ts
|
|
174
|
+
├── prompt-templates.spec.ts
|
|
175
|
+
├── profile.spec.ts
|
|
176
|
+
├── settings.spec.ts
|
|
177
|
+
└── users.spec.ts
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## NEVER
|
|
181
|
+
|
|
182
|
+
- **NEVER** use CSS selectors (`.class`, `#id`, `div > span`, `[class*="..."]`)
|
|
183
|
+
- **NEVER** use `expect(await el.isVisible()).toBe(true)` — use `await expect(el).toBeVisible()`
|
|
184
|
+
- **NEVER** use `page.waitForTimeout()`
|
|
185
|
+
- **NEVER** use XPath selectors
|
|
186
|
+
- **NEVER** test third-party services directly — mock with `page.route()`
|
|
187
|
+
- **NEVER** use `localStorage`/`sessionStorage` to mock auth — mock `/auth/me` via `page.route()`
|
|
188
|
+
- **NEVER** use `fireEvent` — use Playwright's built-in actions (`click`, `fill`, `press`)
|
|
189
|
+
- **NEVER** write happy-path-only tests — all 4 states are mandatory
|
|
190
|
+
- **NEVER** use `.animate-spin` or other CSS class selectors for loading states — use `[role="status"]` or semantic text
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: "src/**/*.spec.ts, src/**/*.test.ts, client/src/**/*.test.{ts,tsx}, client/e2e/**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Testing Patterns
|
|
6
|
+
|
|
7
|
+
## Backend — Three Layers
|
|
8
|
+
|
|
9
|
+
### Unit Tests (Jest)
|
|
10
|
+
|
|
11
|
+
Test handlers, validators, and utilities in isolation.
|
|
12
|
+
|
|
13
|
+
Every handler test must cover: happy path, validation failure, not found, conflict, permission check.
|
|
14
|
+
|
|
15
|
+
Validator tests are mandatory and separate.
|
|
16
|
+
|
|
17
|
+
### Integration Tests (Jest + Supertest)
|
|
18
|
+
|
|
19
|
+
Full HTTP → Controller → Pipeline → Handler → Database round trips.
|
|
20
|
+
|
|
21
|
+
**Real database, not mocks.** Run against PostgreSQL via `docker-compose.test.yml`.
|
|
22
|
+
|
|
23
|
+
### E2E Tests
|
|
24
|
+
|
|
25
|
+
Isolated test stack: `docker-compose.test.yml` with separate ports (API 3099, Keycloak 8099).
|
|
26
|
+
|
|
27
|
+
## Frontend — Three Layers
|
|
28
|
+
|
|
29
|
+
### Component Tests (Vitest + React Testing Library)
|
|
30
|
+
|
|
31
|
+
Test user-visible behavior, not implementation details.
|
|
32
|
+
|
|
33
|
+
**Query priority:** `getByRole` > `getByLabelText` > `getByText` > `getByTestId` (last resort).
|
|
34
|
+
|
|
35
|
+
**Every component test must cover:** happy path, loading state, error state, empty state, user interactions.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
it('renders item name', () => {
|
|
39
|
+
render(<ItemCard item={{ id: '1', name: 'Test' }} />);
|
|
40
|
+
expect(screen.getByRole('heading', { name: 'Test' })).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Hook Tests (Vitest + TanStack Query)
|
|
45
|
+
|
|
46
|
+
Test custom hooks with a real QueryClient. Use MSW for API mocking.
|
|
47
|
+
|
|
48
|
+
**Auth mocking:**
|
|
49
|
+
```typescript
|
|
50
|
+
// ✅ CORRECT
|
|
51
|
+
queryClient.setQueryData(['auth', 'session'], { keycloakId: 'test-id', roles: ['user'] });
|
|
52
|
+
|
|
53
|
+
// ❌ WRONG
|
|
54
|
+
localStorage.setItem('access_token', 'fake-jwt');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### E2E Tests (Playwright)
|
|
58
|
+
|
|
59
|
+
Mock APIs via `page.route()`. Every spec covers 4 states:
|
|
60
|
+
|
|
61
|
+
| State | Mock |
|
|
62
|
+
|-------|------|
|
|
63
|
+
| Happy path | API returns 200 |
|
|
64
|
+
| Error | API returns 500 |
|
|
65
|
+
| Empty | API returns 200 + empty array |
|
|
66
|
+
| Loading | Delay API response |
|
|
67
|
+
|
|
68
|
+
**Auth in E2E:**
|
|
69
|
+
```typescript
|
|
70
|
+
await page.route('**/auth/me', route => route.fulfill({
|
|
71
|
+
status: 200,
|
|
72
|
+
body: JSON.stringify({ keycloakId: 'kc-1', roles: ['user'] }),
|
|
73
|
+
}));
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Responsive testing:** Test at mobile viewport (375×812) and desktop.
|
|
77
|
+
|
|
78
|
+
## CI Pipeline
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
PR → lint → type-check → unit tests → integration tests → UI E2E → merge
|
|
82
|
+
Nightly → system E2E (full stack)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## NEVER
|
|
86
|
+
|
|
87
|
+
- **NEVER** test implementation details (internal state, private methods, CSS classes)
|
|
88
|
+
- **NEVER** use CSS selectors in E2E tests — use semantic locators
|
|
89
|
+
- **NEVER** use `page.waitForTimeout()` — use web-first assertions
|
|
90
|
+
- **NEVER** use `fireEvent` — use `userEvent`
|
|
91
|
+
- **NEVER** mock the database in integration tests
|
|
92
|
+
- **NEVER** use `localStorage`/`sessionStorage` for auth in tests
|
|
93
|
+
- **NEVER** write happy-path-only tests
|
|
94
|
+
- **NEVER** use `setTimeout`/`waitForTimeout` for timing in tests
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Workflow Backend — Use the Workflow Engine, Don't Rebuild It
|
|
2
|
+
|
|
3
|
+
The workflow engine handles process orchestration, task routing, and state management. **Application code defines workflow configurations and domain-specific handlers — never reimplements the engine.**
|
|
4
|
+
|
|
5
|
+
## Workflow Module (`src/workflow/`)
|
|
6
|
+
|
|
7
|
+
This module owns all workflow engine interaction. Other modules (change-request, notification, etc.) consume workflow events and provide domain-specific handlers — they never access workflow internals directly.
|
|
8
|
+
|
|
9
|
+
## Engine Responsibilities
|
|
10
|
+
|
|
11
|
+
| Concern | Handled by |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Process instance lifecycle (start, signal, abort) | Workflow module commands |
|
|
14
|
+
| Task assignment and routing | Workflow engine based on process definitions |
|
|
15
|
+
| Task claim/unclaim/complete | Workflow module commands dispatched from controllers |
|
|
16
|
+
| Process variable management | Workflow engine context — read via workflow queries |
|
|
17
|
+
| Workflow state transitions | Process definitions — not application code |
|
|
18
|
+
|
|
19
|
+
## What Goes in Application Code vs. Workflow Engine
|
|
20
|
+
|
|
21
|
+
| Belongs in **application code** | Belongs in **workflow engine** (already built) |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Workflow definition configs (which roles, which steps) | Process execution and state machine logic |
|
|
24
|
+
| Domain-specific task completion handlers | Task routing and assignment rules |
|
|
25
|
+
| Commands to start/signal/complete workflow steps | Process instance lifecycle management |
|
|
26
|
+
| Validators for task completion payloads | State transition persistence |
|
|
27
|
+
| Event consumers reacting to workflow state changes | Process evaluation and branching |
|
|
28
|
+
| Role and permission definitions for workflow steps | Task inbox queries and filtering |
|
|
29
|
+
|
|
30
|
+
## Inter-Module Communication
|
|
31
|
+
|
|
32
|
+
Other modules interact with workflows through the bus — never by importing workflow services directly:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// ✅ CORRECT — dispatch via CommandBus
|
|
36
|
+
this.commandBus.execute(new StartWorkflowCommand(processId, variables));
|
|
37
|
+
this.commandBus.execute(new CompleteTaskCommand(taskId, payload));
|
|
38
|
+
this.queryBus.execute(new GetProcessTimelineQuery(processInstanceId));
|
|
39
|
+
|
|
40
|
+
// ❌ WRONG — direct service import from another module
|
|
41
|
+
await this.workflowService.startProcess('cr-approval', variables);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Workflow Events via Redis Streams
|
|
45
|
+
|
|
46
|
+
Workflow state changes are published to Redis Streams. Domain modules subscribe to react:
|
|
47
|
+
|
|
48
|
+
| Event | Published when |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `workflow.process.started` | New process instance created |
|
|
51
|
+
| `workflow.task.assigned` | Task routed to a user/role |
|
|
52
|
+
| `workflow.task.completed` | Task action submitted |
|
|
53
|
+
| `workflow.process.completed` | Process reaches end state |
|
|
54
|
+
| `workflow.process.error` | Process hits an error boundary |
|
|
55
|
+
|
|
56
|
+
## NEVER
|
|
57
|
+
|
|
58
|
+
- **NEVER** access workflow internals from outside the workflow module — dispatch commands via the bus
|
|
59
|
+
- **NEVER** write your own process state machine — use the workflow engine
|
|
60
|
+
- **NEVER** write your own task assignment/routing logic — configure it in process definitions
|
|
61
|
+
- **NEVER** write your own task inbox query — use workflow module queries
|
|
62
|
+
- **NEVER** manage process variables outside the workflow module — read them via workflow queries
|
|
63
|
+
- **NEVER** poll for workflow state changes — subscribe to workflow Redis Stream events
|
|
64
|
+
- **NEVER** import workflow module services into other modules — use CommandBus/QueryBus
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Workflow UI — Use `@quanticjs/workflow-ui`, Don't Rebuild It
|
|
2
|
+
|
|
3
|
+
`@quanticjs/workflow-ui` provides the **complete workflow UI layer** for task management. Every component and hook below is already implemented. Import and use it — never write your own version.
|
|
4
|
+
|
|
5
|
+
## Provider
|
|
6
|
+
|
|
7
|
+
| Export | Purpose |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `WorkflowProvider` | Context provider for workflow hooks — wraps pages that use workflow features |
|
|
10
|
+
|
|
11
|
+
## Hooks
|
|
12
|
+
|
|
13
|
+
| Hook | Purpose |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `useTaskList(filters?)` | Fetches task inbox — filterable by status, role, assignee |
|
|
16
|
+
| `useTask(taskId)` | Fetches single task with full context (process variables, form schema) |
|
|
17
|
+
| `useTaskClaim(taskId)` | Claim/unclaim a task for the current user |
|
|
18
|
+
| `useTaskAction(taskId)` | Complete, delegate, or skip a task with payload |
|
|
19
|
+
| `useProcessTimeline(processInstanceId)` | Fetches workflow state history — completed/pending/active nodes |
|
|
20
|
+
|
|
21
|
+
## Components
|
|
22
|
+
|
|
23
|
+
| Component | Purpose |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `TaskInbox` | Full task list with filtering, sorting, pagination — renders role-based task assignments |
|
|
26
|
+
| `WorkflowForm` | Dynamic form rendered from task form schema — integrates with `useTaskAction` |
|
|
27
|
+
| `TaskDetail` | Task context view — process variables, form, history, actions |
|
|
28
|
+
| `TaskActions` | Approve/reject/claim/delegate action buttons — wired to `useTaskAction` |
|
|
29
|
+
| `ProcessTimeline` | Visual workflow state diagram — completed (green), active (yellow), blocked (red) |
|
|
30
|
+
|
|
31
|
+
## When to Use
|
|
32
|
+
|
|
33
|
+
| Feature need | Use |
|
|
34
|
+
|---|---|
|
|
35
|
+
| Dashboard task list | `TaskInbox` |
|
|
36
|
+
| Approval interfaces | `TaskDetail` + `TaskActions` |
|
|
37
|
+
| Workflow visualization | `ProcessTimeline` |
|
|
38
|
+
| Dynamic task forms | `WorkflowForm` |
|
|
39
|
+
| Claiming tasks | `useTaskClaim` |
|
|
40
|
+
| Completing workflow steps | `useTaskAction` |
|
|
41
|
+
|
|
42
|
+
## What Goes in Application Code vs. workflow-ui
|
|
43
|
+
|
|
44
|
+
| Belongs in **application code** | Belongs in **workflow-ui** (already built) |
|
|
45
|
+
|---|---|
|
|
46
|
+
| CR-specific approval pages | Task inbox with filtering/sorting/pagination (`TaskInbox`) |
|
|
47
|
+
| Custom approval form layouts composing `TaskDetail` | Task context view + form rendering (`TaskDetail`, `WorkflowForm`) |
|
|
48
|
+
| Page routing to task views | Approve/reject/claim/delegate buttons (`TaskActions`) |
|
|
49
|
+
| Workflow page wrappers with `WorkflowProvider` | Process state visualization (`ProcessTimeline`) |
|
|
50
|
+
| Domain-specific task filters | Task fetching, claiming, action hooks |
|
|
51
|
+
|
|
52
|
+
## NEVER
|
|
53
|
+
|
|
54
|
+
- **NEVER** write your own task inbox component — use `TaskInbox` from `@quanticjs/workflow-ui`
|
|
55
|
+
- **NEVER** write your own task detail/actions UI — use `TaskDetail` + `TaskActions` from `@quanticjs/workflow-ui`
|
|
56
|
+
- **NEVER** write your own workflow visualization — use `ProcessTimeline` from `@quanticjs/workflow-ui`
|
|
57
|
+
- **NEVER** write your own dynamic form renderer for workflow tasks — use `WorkflowForm` from `@quanticjs/workflow-ui`
|
|
58
|
+
- **NEVER** write your own task list/claim/complete hooks — use `useTaskList` / `useTaskClaim` / `useTaskAction` from `@quanticjs/workflow-ui`
|
|
59
|
+
- **NEVER** write your own process timeline hook — use `useProcessTimeline` from `@quanticjs/workflow-ui`
|
|
60
|
+
- **NEVER** use workflow hooks outside a `WorkflowProvider` — wrap workflow pages with it
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npm install *)",
|
|
5
|
+
"Bash(npm ci)",
|
|
6
|
+
"Bash(npm run *)",
|
|
7
|
+
"Bash(npm test *)",
|
|
8
|
+
"Bash(npm init *)",
|
|
9
|
+
"Bash(npx *)",
|
|
10
|
+
"Bash(node *)",
|
|
11
|
+
"Bash(ls *)",
|
|
12
|
+
"Bash(find *)",
|
|
13
|
+
"Bash(grep *)",
|
|
14
|
+
"Bash(cat *)",
|
|
15
|
+
"Bash(mkdir *)",
|
|
16
|
+
"Bash(cp *)",
|
|
17
|
+
"Bash(mv *)",
|
|
18
|
+
"Bash(git status*)",
|
|
19
|
+
"Bash(git log*)",
|
|
20
|
+
"Bash(git diff*)",
|
|
21
|
+
"Bash(git branch*)",
|
|
22
|
+
"Bash(git add *)",
|
|
23
|
+
"Bash(git commit *)",
|
|
24
|
+
"Bash(docker compose *)",
|
|
25
|
+
"Bash(docker build *)",
|
|
26
|
+
"Bash(docker ps*)"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"hooks": {
|
|
30
|
+
"PreToolUse": [
|
|
31
|
+
{
|
|
32
|
+
"matcher": "Bash",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-destructive.sh\"",
|
|
37
|
+
"timeout": 5
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "command",
|
|
41
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/check-secrets.sh\"",
|
|
42
|
+
"timeout": 10
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/auto-format.sh\"",
|
|
47
|
+
"timeout": 15
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"Notification": [
|
|
53
|
+
{
|
|
54
|
+
"matcher": "compact",
|
|
55
|
+
"hooks": [
|
|
56
|
+
{
|
|
57
|
+
"type": "command",
|
|
58
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/on-compaction.sh\"",
|
|
59
|
+
"timeout": 10
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"enabledPlugins": {
|
|
66
|
+
"frontend-design@claude-plugins-official": true
|
|
67
|
+
}
|
|
68
|
+
}
|