nextjs-hackathon-stack 0.1.32 → 0.1.33
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/index.js +157 -10
- package/package.json +1 -1
- package/template/.cursor/agents/backend.md +18 -0
- package/template/.cursor/agents/business-intelligence.md +16 -1
- package/template/.cursor/agents/code-reviewer.md +3 -0
- package/template/.cursor/agents/frontend.md +18 -0
- package/template/.cursor/agents/technical-lead.md +4 -0
- package/template/.cursor/agents/test-qa.md +37 -1
- package/template/.cursor/mcp.json +8 -0
- package/template/.cursor/memory/architecture-snapshot.md +50 -0
- package/template/.cursor/rules/coding-standards.mdc +3 -12
- package/template/.cursor/rules/components.mdc +7 -0
- package/template/.cursor/rules/migrations.mdc +2 -19
- package/template/.cursor/rules/supabase.mdc +14 -2
- package/template/.cursor/skills/create-feature/SKILL.md +78 -15
- package/template/.cursor/skills/create-feature/references/server-action-test-template.md +92 -40
- package/template/CLAUDE.md +10 -0
- package/template/src/app/(protected)/page.tsx +1 -1
- package/template/src/features/todos/components/add-todo-form.tsx +1 -1
- package/template/src/features/todos/components/todo-list.tsx +4 -4
- package/template/src/shared/components/providers.tsx +10 -1
- package/template/src/shared/test-utils/supabase-mock.ts +59 -0
- package/template/tailwind.css +6 -0
package/dist/index.js
CHANGED
|
@@ -43,13 +43,25 @@ async function runCli(argProjectName, skipInstall) {
|
|
|
43
43
|
}
|
|
44
44
|
projectName = result;
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
const templateResult = await p2.select({
|
|
47
|
+
message: "What would you like to start with?",
|
|
48
|
+
options: [
|
|
49
|
+
{ value: "example", label: "Todo example app", hint: "Full working app with todos CRUD" },
|
|
50
|
+
{ value: "empty", label: "Empty app", hint: "Auth + empty dashboard" }
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
if (p2.isCancel(templateResult)) {
|
|
54
|
+
p2.cancel("Cancelled");
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
const template = templateResult;
|
|
58
|
+
return { projectName, skipInstall, template };
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
// src/scaffold.ts
|
|
50
62
|
import { execSync } from "child_process";
|
|
51
|
-
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync,
|
|
52
|
-
import { join, dirname } from "path";
|
|
63
|
+
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, symlinkSync, writeFileSync } from "fs";
|
|
64
|
+
import { join, dirname, resolve } from "path";
|
|
53
65
|
import { fileURLToPath } from "url";
|
|
54
66
|
import * as p3 from "@clack/prompts";
|
|
55
67
|
import pc2 from "picocolors";
|
|
@@ -60,7 +72,7 @@ function getTemplateDir() {
|
|
|
60
72
|
function processTemplate(content, vars) {
|
|
61
73
|
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
|
|
62
74
|
}
|
|
63
|
-
function copyDir(src, dest, vars) {
|
|
75
|
+
function copyDir(src, dest, vars, skip = /* @__PURE__ */ new Set(), templateRoot = src) {
|
|
64
76
|
mkdirSync(dest, { recursive: true });
|
|
65
77
|
for (const entry of readdirSync(src)) {
|
|
66
78
|
const srcPath = join(src, entry);
|
|
@@ -70,11 +82,18 @@ function copyDir(src, dest, vars) {
|
|
|
70
82
|
}
|
|
71
83
|
const destPath = join(dest, destEntry);
|
|
72
84
|
const stat = lstatSync(srcPath);
|
|
85
|
+
const relPath = srcPath.slice(templateRoot.length + 1).replace(/\\/g, "/");
|
|
86
|
+
if (skip.has(relPath)) continue;
|
|
73
87
|
if (stat.isSymbolicLink()) {
|
|
74
88
|
const target = readlinkSync(srcPath);
|
|
75
|
-
|
|
89
|
+
const resolvedTarget = resolve(dirname(srcPath), target);
|
|
90
|
+
if (existsSync(resolvedTarget) && lstatSync(resolvedTarget).isFile()) {
|
|
91
|
+
cpSync(resolvedTarget, destPath);
|
|
92
|
+
} else {
|
|
93
|
+
symlinkSync(target, destPath);
|
|
94
|
+
}
|
|
76
95
|
} else if (stat.isDirectory()) {
|
|
77
|
-
copyDir(srcPath, destPath, vars);
|
|
96
|
+
copyDir(srcPath, destPath, vars, skip, templateRoot);
|
|
78
97
|
} else if (entry.endsWith(".tmpl")) {
|
|
79
98
|
const content = readFileSync(srcPath, "utf-8");
|
|
80
99
|
writeFileSync(destPath, processTemplate(content, vars));
|
|
@@ -83,7 +102,97 @@ function copyDir(src, dest, vars) {
|
|
|
83
102
|
}
|
|
84
103
|
}
|
|
85
104
|
}
|
|
86
|
-
|
|
105
|
+
var EMPTY_APP_PAGE = `import { redirect } from "next/navigation";
|
|
106
|
+
import { logoutAction } from "@/features/auth/actions/logout.action";
|
|
107
|
+
import { createClient } from "@/shared/lib/supabase/server";
|
|
108
|
+
|
|
109
|
+
export default async function HomePage() {
|
|
110
|
+
const supabase = await createClient();
|
|
111
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
112
|
+
if (!user) redirect("/login");
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<main className="container mx-auto max-w-2xl p-8">
|
|
116
|
+
<div className="mb-6 flex items-center justify-between">
|
|
117
|
+
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
118
|
+
<form action={logoutAction}>
|
|
119
|
+
<button type="submit" className="rounded bg-muted px-4 py-2 text-sm hover:bg-muted-foreground/10">
|
|
120
|
+
Cerrar sesi\xF3n
|
|
121
|
+
</button>
|
|
122
|
+
</form>
|
|
123
|
+
</div>
|
|
124
|
+
<p className="text-muted-foreground">\xA1Bienvenido! Empieza a construir tu aplicaci\xF3n.</p>
|
|
125
|
+
</main>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
var EMPTY_APP_SCHEMA = `import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
130
|
+
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
|
131
|
+
|
|
132
|
+
export const profiles = pgTable("profiles", {
|
|
133
|
+
id: uuid("id").primaryKey(),
|
|
134
|
+
email: text("email").notNull().unique(),
|
|
135
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
136
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export const insertProfileSchema = createInsertSchema(profiles);
|
|
140
|
+
export const selectProfileSchema = createSelectSchema(profiles);
|
|
141
|
+
|
|
142
|
+
export type InsertProfile = typeof profiles.$inferInsert;
|
|
143
|
+
export type SelectProfile = typeof profiles.$inferSelect;
|
|
144
|
+
`;
|
|
145
|
+
var EMPTY_APP_PAGE_TEST = `import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
146
|
+
|
|
147
|
+
const mockGetUser = vi.fn();
|
|
148
|
+
const mockRedirect = vi.fn();
|
|
149
|
+
|
|
150
|
+
vi.mock("@/shared/lib/supabase/server", () => ({
|
|
151
|
+
createClient: vi.fn(() =>
|
|
152
|
+
Promise.resolve({ auth: { getUser: mockGetUser } })
|
|
153
|
+
),
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
vi.mock("next/navigation", () => ({
|
|
157
|
+
redirect: (url: string): unknown => mockRedirect(url),
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
describe("HomePage (protected)", () => {
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
vi.clearAllMocks();
|
|
163
|
+
mockRedirect.mockImplementation(() => { throw new Error("NEXT_REDIRECT"); });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("redirects to /login when user is not authenticated", async () => {
|
|
167
|
+
// Arrange
|
|
168
|
+
mockGetUser.mockResolvedValue({ data: { user: null } });
|
|
169
|
+
const { default: HomePage } = await import("../(protected)/page");
|
|
170
|
+
|
|
171
|
+
// Act + Assert
|
|
172
|
+
await expect(HomePage()).rejects.toThrow("NEXT_REDIRECT");
|
|
173
|
+
expect(mockRedirect).toHaveBeenCalledWith("/login");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("renders dashboard when user is authenticated", async () => {
|
|
177
|
+
// Arrange
|
|
178
|
+
mockGetUser.mockResolvedValue({ data: { user: { id: "u1", email: "user@example.com" } } });
|
|
179
|
+
const { default: HomePage } = await import("../(protected)/page");
|
|
180
|
+
|
|
181
|
+
// Act
|
|
182
|
+
const result = await HomePage();
|
|
183
|
+
|
|
184
|
+
// Assert
|
|
185
|
+
expect(result).toBeTruthy();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
`;
|
|
189
|
+
function buildSkipSet() {
|
|
190
|
+
return /* @__PURE__ */ new Set([
|
|
191
|
+
"src/features/todos",
|
|
192
|
+
"src/e2e/todos.spec.ts"
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
async function scaffold(projectName, skipInstall, skipMcp = false, template = "example") {
|
|
87
196
|
const targetDir = join(process.cwd(), projectName);
|
|
88
197
|
if (existsSync(targetDir)) {
|
|
89
198
|
p3.cancel(`Directory "${projectName}" already exists`);
|
|
@@ -91,9 +200,19 @@ async function scaffold(projectName, skipInstall) {
|
|
|
91
200
|
}
|
|
92
201
|
const templateDir = getTemplateDir();
|
|
93
202
|
const vars = { projectName };
|
|
203
|
+
const skip = template === "empty" ? buildSkipSet() : /* @__PURE__ */ new Set();
|
|
94
204
|
const spinner2 = p3.spinner();
|
|
95
205
|
spinner2.start("Copying template files");
|
|
96
|
-
copyDir(templateDir, targetDir, vars);
|
|
206
|
+
copyDir(templateDir, targetDir, vars, skip, templateDir);
|
|
207
|
+
if (template === "empty") {
|
|
208
|
+
const pageDir = join(targetDir, "src/app/(protected)");
|
|
209
|
+
mkdirSync(pageDir, { recursive: true });
|
|
210
|
+
writeFileSync(join(pageDir, "page.tsx"), EMPTY_APP_PAGE);
|
|
211
|
+
writeFileSync(join(targetDir, "src/shared/db/schema.ts"), EMPTY_APP_SCHEMA);
|
|
212
|
+
const testDir = join(targetDir, "src/app/__tests__");
|
|
213
|
+
mkdirSync(testDir, { recursive: true });
|
|
214
|
+
writeFileSync(join(testDir, "protected-page.test.tsx"), EMPTY_APP_PAGE_TEST);
|
|
215
|
+
}
|
|
97
216
|
spinner2.stop("Template files copied");
|
|
98
217
|
const envExamplePath = join(targetDir, ".env.example");
|
|
99
218
|
const envLocalPath = join(targetDir, ".env.local");
|
|
@@ -154,6 +273,34 @@ async function scaffold(projectName, skipInstall) {
|
|
|
154
273
|
} catch {
|
|
155
274
|
spinner2.stop("Husky setup failed \u2014 run manually: pnpm exec husky");
|
|
156
275
|
}
|
|
276
|
+
if (!skipMcp) {
|
|
277
|
+
spinner2.start("Installing mcp-memory-service");
|
|
278
|
+
try {
|
|
279
|
+
const pipCmd = (() => {
|
|
280
|
+
try {
|
|
281
|
+
execSync("pip3 --version", { stdio: "pipe" });
|
|
282
|
+
return "pip3";
|
|
283
|
+
} catch {
|
|
284
|
+
try {
|
|
285
|
+
execSync("pip --version", { stdio: "pipe" });
|
|
286
|
+
return "pip";
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
})();
|
|
292
|
+
if (pipCmd) {
|
|
293
|
+
execSync(`${pipCmd} install mcp-memory-service`, { cwd: targetDir, stdio: "pipe" });
|
|
294
|
+
spinner2.stop("mcp-memory-service installed");
|
|
295
|
+
} else {
|
|
296
|
+
spinner2.stop("pip not found \u2014 skipping mcp-memory-service");
|
|
297
|
+
p3.log.warn("Install Python and run: pip install mcp-memory-service");
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
spinner2.stop("mcp-memory-service installation failed");
|
|
301
|
+
p3.log.warn("Install manually: pip install mcp-memory-service");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
157
304
|
success(pc2.bold(`\xA1Proyecto "${projectName}" creado!`));
|
|
158
305
|
console.log(`
|
|
159
306
|
${pc2.dim("Siguientes pasos:")}`);
|
|
@@ -170,10 +317,10 @@ async function scaffold(projectName, skipInstall) {
|
|
|
170
317
|
|
|
171
318
|
// src/index.ts
|
|
172
319
|
var program = new Command();
|
|
173
|
-
program.name("nextjs-hackathon-stack").description("Scaffold a full-stack Next.js 15 hackathon starter").version("0.1.0").argument("[project-name]", "Name of the project to create").option("--skip-install", "Skip pnpm install and shadcn/ui init", false).action(async (projectName, opts) => {
|
|
320
|
+
program.name("nextjs-hackathon-stack").description("Scaffold a full-stack Next.js 15 hackathon starter").version("0.1.0").argument("[project-name]", "Name of the project to create").option("--skip-install", "Skip pnpm install and shadcn/ui init", false).option("--skip-mcp", "Skip mcp-memory-service installation", false).action(async (projectName, opts) => {
|
|
174
321
|
try {
|
|
175
322
|
const options = await runCli(projectName, opts.skipInstall);
|
|
176
|
-
await scaffold(options.projectName, options.skipInstall);
|
|
323
|
+
await scaffold(options.projectName, options.skipInstall, opts.skipMcp, options.template);
|
|
177
324
|
outro2("Happy hacking! \u{1F680}");
|
|
178
325
|
} catch (err) {
|
|
179
326
|
p4.cancel(err instanceof Error ? err.message : "Something went wrong");
|
package/package.json
CHANGED
|
@@ -7,6 +7,10 @@ readonly: false
|
|
|
7
7
|
|
|
8
8
|
# Backend Agent
|
|
9
9
|
|
|
10
|
+
## First Step
|
|
11
|
+
|
|
12
|
+
Read `.cursor/memory/architecture-snapshot.md` to understand the existing schema, features, and proven Server Action patterns before writing new code. Use the canonical pattern references to copy the correct structure.
|
|
13
|
+
|
|
10
14
|
## Responsibilities
|
|
11
15
|
- Build Next.js Server Actions, API routes, and middleware
|
|
12
16
|
- Define Drizzle ORM schemas and manage migrations via CLI
|
|
@@ -23,6 +27,20 @@ readonly: false
|
|
|
23
27
|
- Default to **Node.js runtime** for Server Actions and API routes
|
|
24
28
|
- Use **Edge runtime** only when latency is critical and no Node.js APIs are needed
|
|
25
29
|
|
|
30
|
+
## RLS Policy Requirement
|
|
31
|
+
|
|
32
|
+
RLS is not optional and is never a post-implementation step. When you create a new table:
|
|
33
|
+
|
|
34
|
+
1. Enable RLS on the table in Supabase
|
|
35
|
+
2. Write the RLS policies before the feature is considered done. Default template:
|
|
36
|
+
```sql
|
|
37
|
+
-- Users can only access their own rows
|
|
38
|
+
CREATE POLICY "users_own_rows" ON <table>
|
|
39
|
+
FOR ALL USING (user_id = auth.uid());
|
|
40
|
+
```
|
|
41
|
+
3. Document the policy in the architecture snapshot under the table entry
|
|
42
|
+
4. Never leave "add RLS later" as a TODO — a table without RLS is a security vulnerability
|
|
43
|
+
|
|
26
44
|
## Dev Server
|
|
27
45
|
- Before running `next dev`, check `.next/dev/lock`. If it exists and the PID is alive, an instance is already running — use that server's URL instead of starting a new one.
|
|
28
46
|
- Enable `logging.browserToTerminal` in `next.config.ts` for terminal-based debugging (recommended: `'error'` for production-like sessions, `true` during active development).
|
|
@@ -7,6 +7,10 @@ readonly: true
|
|
|
7
7
|
|
|
8
8
|
# Business Intelligence Agent
|
|
9
9
|
|
|
10
|
+
## First Step
|
|
11
|
+
|
|
12
|
+
Read `.cursor/memory/architecture-snapshot.md` to understand existing features before asking discovery questions. Use it to identify relationships with existing data and patterns.
|
|
13
|
+
|
|
10
14
|
## Responsibilities
|
|
11
15
|
- Discover requirements through user questions before defining anything
|
|
12
16
|
- Write functional issues describing user-visible behavior
|
|
@@ -20,10 +24,21 @@ readonly: true
|
|
|
20
24
|
|
|
21
25
|
Never assume requirements. Always ask first.
|
|
22
26
|
|
|
27
|
+
Complete ALL of these before writing a spec:
|
|
28
|
+
|
|
23
29
|
1. **Problem & audience** — "What problem does this solve? Who experiences it?"
|
|
24
30
|
2. **User flows** — "Walk me through the happy path. What happens on error?"
|
|
25
31
|
3. **Edge cases & constraints** — "What are the limits? What should NOT happen?"
|
|
26
|
-
4. **
|
|
32
|
+
4. **Field constraints** — "What are the length limits, allowed formats, required vs optional fields?"
|
|
33
|
+
5. **Volume & scale** — "How many records are expected? Do you need search or pagination?"
|
|
34
|
+
6. **File/upload specifics** — (if applicable) "What file types and size limits are allowed?"
|
|
35
|
+
7. **Privacy & access** — "Who can see this data? Is it per-user or shared?"
|
|
36
|
+
8. **Relationship to existing features** — (informed by the snapshot) "Does this link to existing data?"
|
|
37
|
+
9. **Confirm understanding** — Restate what you heard and ask for approval before writing anything
|
|
38
|
+
|
|
39
|
+
**Minimum gate:** Cover at least items 1–3 + any that apply from 4–8 before writing.
|
|
40
|
+
|
|
41
|
+
If the user says "just do it" without answering, document all assumptions explicitly in an `## Assumptions` section at the top of the spec.
|
|
27
42
|
|
|
28
43
|
Only after the user confirms your understanding should you produce a functional issue.
|
|
29
44
|
|
|
@@ -59,6 +59,9 @@ glab --version 2>/dev/null && echo "GitLab"
|
|
|
59
59
|
- [ ] Semantic HTML
|
|
60
60
|
- [ ] ARIA labels where needed
|
|
61
61
|
|
|
62
|
+
### Styling
|
|
63
|
+
- [ ] No hardcoded Tailwind palette colors — only design tokens from `tailwind.css`
|
|
64
|
+
|
|
62
65
|
## Output Format
|
|
63
66
|
```
|
|
64
67
|
## Review: <branch/PR name>
|
|
@@ -7,6 +7,10 @@ readonly: false
|
|
|
7
7
|
|
|
8
8
|
# Frontend Agent
|
|
9
9
|
|
|
10
|
+
## First Step
|
|
11
|
+
|
|
12
|
+
Read `.cursor/memory/architecture-snapshot.md` to see which shadcn/ui components are already installed before running `shadcn add`. Use the canonical component test reference to follow the established testing pattern.
|
|
13
|
+
|
|
10
14
|
## Responsibilities
|
|
11
15
|
- Build accessible React components with shadcn/ui and Tailwind v4
|
|
12
16
|
- Enforce Server vs Client component boundaries
|
|
@@ -35,6 +39,19 @@ export function InteractiveComponent() {
|
|
|
35
39
|
}
|
|
36
40
|
```
|
|
37
41
|
|
|
42
|
+
## Common UI Patterns
|
|
43
|
+
|
|
44
|
+
Use these shadcn components for standard layouts — never roll custom alternatives:
|
|
45
|
+
|
|
46
|
+
| Pattern | shadcn Component | Notes |
|
|
47
|
+
|---------|-----------------|-------|
|
|
48
|
+
| Master-detail (table + side panel) | `ResizablePanelGroup` + `ResizablePanel` | Or CSS grid for fixed layouts |
|
|
49
|
+
| Dialog / modal form | `Dialog`, `DialogContent`, `DialogHeader` | Never use native `<dialog>` — breaks in jsdom |
|
|
50
|
+
| Data table | `Table`, `TableHeader`, `TableRow`, `TableCell` | Or `DataTable` pattern from shadcn docs |
|
|
51
|
+
| Empty state | Centered `<p>` in Spanish inside the table container | Always handle — never leave blank |
|
|
52
|
+
| Loading/pending | `Button` with `loading` prop, or `Spinner` | Use `useTransition` for action pending state |
|
|
53
|
+
| Form with validation | `Form` + `FormField` from shadcn + `zodResolver` | See `forms.mdc` |
|
|
54
|
+
|
|
38
55
|
## Toast Feedback
|
|
39
56
|
|
|
40
57
|
All user-initiated actions that modify the database must show a toast (success or error) via `sonner`. Never let a mutation complete silently.
|
|
@@ -48,3 +65,4 @@ All user-initiated actions that modify the database must show a toast (success o
|
|
|
48
65
|
- Write RTL tests for every component
|
|
49
66
|
- Verify a11y before marking work done
|
|
50
67
|
- No component file over 200 lines — split by responsibility
|
|
68
|
+
- Never use raw Tailwind palette colors (`red-500`, `gray-400`, etc.) — only design tokens from `@theme {}` in `tailwind.css`
|
|
@@ -9,6 +9,10 @@ readonly: false
|
|
|
9
9
|
|
|
10
10
|
# Technical Lead Agent
|
|
11
11
|
|
|
12
|
+
## First Step
|
|
13
|
+
|
|
14
|
+
Read `.cursor/memory/architecture-snapshot.md` to understand the current project state before planning. Use it to identify which patterns to reuse and what already exists.
|
|
15
|
+
|
|
12
16
|
## Responsibilities
|
|
13
17
|
- Act as the default entry point for all requests
|
|
14
18
|
- Classify requests and delegate to the appropriate specialist agent(s)
|
|
@@ -7,12 +7,30 @@ readonly: false
|
|
|
7
7
|
|
|
8
8
|
# Test & QA Agent
|
|
9
9
|
|
|
10
|
+
## First Step
|
|
11
|
+
|
|
12
|
+
Read `.cursor/memory/architecture-snapshot.md` to find the canonical test references. Import `makeChain` and `makeSupabaseMock` from `@/shared/test-utils/supabase-mock` — never recreate the mock chain from scratch.
|
|
13
|
+
|
|
10
14
|
## TDD Workflow
|
|
15
|
+
|
|
11
16
|
1. **RED** — write failing test that describes the desired behavior
|
|
12
17
|
2. **GREEN** — write minimum code to make test pass
|
|
13
18
|
3. **REFACTOR** — clean up while keeping tests green
|
|
14
19
|
4. **VERIFY** — run `pnpm test:coverage` and confirm 100%
|
|
15
20
|
|
|
21
|
+
## Verification Gates (mandatory — show output, do not just claim)
|
|
22
|
+
|
|
23
|
+
Each phase requires pasting the actual terminal output before proceeding:
|
|
24
|
+
|
|
25
|
+
| Gate | Command | Expected output |
|
|
26
|
+
|------|---------|-----------------|
|
|
27
|
+
| After RED | `pnpm test:unit` | All new tests **FAIL** (red) |
|
|
28
|
+
| After GREEN | `pnpm test:unit` | All tests **PASS** (green) |
|
|
29
|
+
| After VERIFY | `pnpm test:coverage` | 100% across all metrics |
|
|
30
|
+
|
|
31
|
+
**Never say "tests pass" or "tests fail" without showing the actual output.**
|
|
32
|
+
**Do NOT write any implementation code until the RED gate output confirms failures.**
|
|
33
|
+
|
|
16
34
|
## Key Rules (auto-loaded by file context)
|
|
17
35
|
- Testing standards, AAA pattern, coverage: `testing.mdc`
|
|
18
36
|
- TDD iron rules: `coding-standards.mdc`
|
|
@@ -30,6 +48,24 @@ After every change:
|
|
|
30
48
|
2. If below 100%, add missing tests
|
|
31
49
|
3. Never mark work complete without full coverage
|
|
32
50
|
|
|
51
|
+
## Supabase Mock Pattern
|
|
52
|
+
|
|
53
|
+
Always use the shared helpers — never inline mock chain construction:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { makeChain, makeSupabaseMock } from "@/shared/test-utils/supabase-mock";
|
|
57
|
+
|
|
58
|
+
// Unauthenticated
|
|
59
|
+
const { mockClient } = makeSupabaseMock({ user: null });
|
|
60
|
+
|
|
61
|
+
// Authenticated + DB success
|
|
62
|
+
const { mockClient, mockFrom } = makeSupabaseMock({ user: { id: "user-1" } });
|
|
63
|
+
mockFrom.mockReturnValue(makeChain({ data: [...], error: null }));
|
|
64
|
+
|
|
65
|
+
// DB error
|
|
66
|
+
mockFrom.mockReturnValue(makeChain({ error: { message: "db error" } }));
|
|
67
|
+
```
|
|
68
|
+
|
|
33
69
|
## Test Plan Compliance
|
|
34
70
|
|
|
35
71
|
When the Technical Lead provides a test plan, follow it:
|
|
@@ -37,7 +73,7 @@ When the Technical Lead provides a test plan, follow it:
|
|
|
37
73
|
- Flag missing edge cases to the TL **before** adding unplanned tests — do not expand scope unilaterally
|
|
38
74
|
- If the plan is ambiguous or incomplete, ask for clarification rather than guessing
|
|
39
75
|
|
|
40
|
-
For Server Action test
|
|
76
|
+
For the full Server Action test template, see `create-feature` skill references.
|
|
41
77
|
|
|
42
78
|
## Guardrails
|
|
43
79
|
- Never write implementation without a failing test first
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Architecture Snapshot
|
|
2
|
+
|
|
3
|
+
> Auto-updated after each feature. Agents MUST read this before exploring the codebase.
|
|
4
|
+
|
|
5
|
+
## Installed shadcn/ui Components
|
|
6
|
+
|
|
7
|
+
button, card, input, label, spinner
|
|
8
|
+
|
|
9
|
+
_Add new components here after `shadcn add <component>`_
|
|
10
|
+
|
|
11
|
+
## DB Schema
|
|
12
|
+
|
|
13
|
+
| Table | Columns |
|
|
14
|
+
|-------|---------|
|
|
15
|
+
| `profiles` | id (uuid PK), email (text unique), createdAt, updatedAt |
|
|
16
|
+
| `todos` | id (uuid PK), userId (uuid), title (text), completed (bool), createdAt |
|
|
17
|
+
|
|
18
|
+
_Add new tables here after `pnpm db:generate && pnpm db:migrate`_
|
|
19
|
+
|
|
20
|
+
## Existing Features
|
|
21
|
+
|
|
22
|
+
| Feature | Path | Description |
|
|
23
|
+
|---------|------|-------------|
|
|
24
|
+
| auth | `src/features/auth/` | Login/logout, cookie-based sessions via Supabase |
|
|
25
|
+
| todos | `src/features/todos/` | CRUD todos with Supabase RLS |
|
|
26
|
+
|
|
27
|
+
_Add new features here after implementation_
|
|
28
|
+
|
|
29
|
+
## Canonical Pattern References
|
|
30
|
+
|
|
31
|
+
Read these files to understand the project's proven patterns before writing new code:
|
|
32
|
+
|
|
33
|
+
| Pattern | File |
|
|
34
|
+
|---------|------|
|
|
35
|
+
| Server Action | `src/features/todos/actions/todos.action.ts` |
|
|
36
|
+
| Query (repository) | `src/features/todos/queries/todos.queries.ts` |
|
|
37
|
+
| Client component | `src/features/todos/components/todo-list.tsx` |
|
|
38
|
+
| Action test | `src/features/todos/__tests__/todos.action.test.ts` |
|
|
39
|
+
| Component test | `src/features/todos/__tests__/todo-list.test.tsx` |
|
|
40
|
+
| Supabase mock chain | `src/shared/test-utils/supabase-mock.ts` |
|
|
41
|
+
|
|
42
|
+
## Key Rules (summary)
|
|
43
|
+
|
|
44
|
+
- Runtime queries: Supabase client only — Drizzle is schema/migrations only
|
|
45
|
+
- All mutations: Server Actions returning `ActionResult` from `@/shared/lib/action-result`
|
|
46
|
+
- Auth check first in every Server Action: `supabase.auth.getUser()`
|
|
47
|
+
- Toast feedback required for every mutation (success + error) via `sonner`
|
|
48
|
+
- RLS must be enabled on every table before a feature is considered complete
|
|
49
|
+
- UI text: Spanish | Code-level text: English (test descriptions, variable names)
|
|
50
|
+
- 100% test coverage — no exceptions
|
|
@@ -14,18 +14,9 @@ globs: ["src/**/*.ts", "src/**/*.tsx"]
|
|
|
14
14
|
- **No `!` non-null assertions** — use proper null checks
|
|
15
15
|
- **No `eslint-disable` inline comments** — fix the code instead. If a rule conflict is truly unavoidable (e.g. third-party API with no non-deprecated alternative), add a file-level override in `eslint.config.ts` with a `// TODO` comment explaining why.
|
|
16
16
|
|
|
17
|
-
## TDD
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
3. **REFACTOR**: Improve code while tests stay green
|
|
21
|
-
|
|
22
|
-
Never write implementation before tests exist.
|
|
23
|
-
|
|
24
|
-
## TDD — Iron Rule
|
|
25
|
-
- Tests define the contract. **NEVER modify a test to make it pass.**
|
|
26
|
-
- If a test fails, the implementation is wrong — fix the code, not the test.
|
|
27
|
-
- Only modify tests to: add new tests (RED), restructure without weakening (REFACTOR).
|
|
28
|
-
- Weakening a test (removing assertions, loosening matchers, adding `.skip`) is a rejection-worthy offense.
|
|
17
|
+
## TDD
|
|
18
|
+
|
|
19
|
+
Follow the RED → GREEN → REFACTOR cycle. See `testing.mdc` for the full rules and iron rules. Summary: never write implementation without a prior failing test.
|
|
29
20
|
|
|
30
21
|
## SOLID Principles
|
|
31
22
|
- **S**ingle Responsibility: one reason to change per module
|
|
@@ -24,6 +24,13 @@ globs: ["src/**/components/**"]
|
|
|
24
24
|
- No inline styles, no CSS modules
|
|
25
25
|
- Use Tailwind utility classes only
|
|
26
26
|
|
|
27
|
+
## Design Tokens (Required)
|
|
28
|
+
- Always use semantic token classes (`bg-primary`, `text-destructive`, `text-muted-foreground`, `border-border`, etc.)
|
|
29
|
+
- Never use raw Tailwind palette colors (`bg-blue-500`, `text-gray-700`, `text-red-500`, etc.)
|
|
30
|
+
- All colors must reference CSS variables defined in `@theme {}` in `tailwind.css`
|
|
31
|
+
- Available semantic colors: primary, secondary, accent, destructive, success, warning, info, muted, background, foreground, border, input, ring
|
|
32
|
+
- If a needed color does not exist as a token, add it to `@theme {}` in `tailwind.css` first
|
|
33
|
+
|
|
27
34
|
## Accessibility (Required)
|
|
28
35
|
- Semantic HTML elements (`<button>`, `<nav>`, `<main>`, etc.)
|
|
29
36
|
- ARIA labels for interactive elements without visible text
|
|
@@ -5,23 +5,6 @@ globs: ["src/shared/db/migrations/**"]
|
|
|
5
5
|
|
|
6
6
|
# Migration Files — Generated Artifacts
|
|
7
7
|
|
|
8
|
-
Files in `src/shared/db/migrations/` are **auto-generated
|
|
8
|
+
Files in `src/shared/db/migrations/` are **auto-generated**. See `supabase.mdc` for the full migration workflow and CLI commands.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
1. **Edit the schema**: `src/shared/db/schema.ts`
|
|
13
|
-
2. **Generate the migration**: `pnpm db:generate`
|
|
14
|
-
3. **Review** the generated SQL file in `src/shared/db/migrations/`
|
|
15
|
-
4. **Apply the migration**: `pnpm db:migrate`
|
|
16
|
-
|
|
17
|
-
## Available CLI Commands
|
|
18
|
-
|
|
19
|
-
| Command | Purpose |
|
|
20
|
-
|---|---|
|
|
21
|
-
| `pnpm db:generate` | Generate a new migration SQL file from schema changes |
|
|
22
|
-
| `pnpm db:migrate` | Apply pending migrations to the database |
|
|
23
|
-
| `pnpm db:push` | Push schema directly to DB (dev only, no migration file) |
|
|
24
|
-
|
|
25
|
-
## Why
|
|
26
|
-
|
|
27
|
-
Hand-editing migration files causes schema drift and breaks the migration history. The CLI is the only safe source of truth for migration SQL.
|
|
10
|
+
**Never create, edit, or delete migration files by hand.**
|
|
@@ -48,8 +48,20 @@ const users = await db.select().from(usersTable); // WRONG — bypasses RLS
|
|
|
48
48
|
|
|
49
49
|
## RLS Mandate
|
|
50
50
|
- Enable RLS on ALL tables in Supabase dashboard
|
|
51
|
-
- Every table must have explicit policies
|
|
52
|
-
-
|
|
51
|
+
- Every table must have explicit policies — RLS is not a post-step
|
|
52
|
+
- Default policy template: `CREATE POLICY "users_own_rows" ON <table> FOR ALL USING (user_id = auth.uid());`
|
|
53
|
+
|
|
54
|
+
## Migrations
|
|
55
|
+
|
|
56
|
+
Files in `src/shared/db/migrations/` are **auto-generated** by Drizzle CLI — never create, edit, or delete them directly.
|
|
57
|
+
|
|
58
|
+
| Command | Purpose |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `pnpm db:generate` | Generate migration SQL from schema changes |
|
|
61
|
+
| `pnpm db:migrate` | Apply pending migrations to the database |
|
|
62
|
+
| `pnpm db:push` | Push schema directly to DB (dev only, no migration file) |
|
|
63
|
+
|
|
64
|
+
Workflow: edit `src/shared/db/schema.ts` → `pnpm db:generate` → review SQL → `pnpm db:migrate`
|
|
53
65
|
|
|
54
66
|
## Environment Variables
|
|
55
67
|
- `NEXT_PUBLIC_SUPABASE_URL` + `NEXT_PUBLIC_SUPABASE_ANON_KEY` — client-safe, used in browser + server
|
|
@@ -5,6 +5,17 @@ description: Scaffold a new feature following TDD and project conventions. Use w
|
|
|
5
5
|
|
|
6
6
|
# Create Feature Skill
|
|
7
7
|
|
|
8
|
+
> **Invoke as:** `/create-feature @.requirements/<feature-name>.md`
|
|
9
|
+
> This skill is orchestrated by the `@technical-lead` agent — do NOT add `use @technical-lead` to the invocation, it is redundant.
|
|
10
|
+
|
|
11
|
+
## IMPORTANT: Run this in a FRESH conversation
|
|
12
|
+
|
|
13
|
+
If this conversation already contains requirements gathering (BI agent output), **start a new conversation** before running `/create-feature`. Reusing the same context risks hitting the token limit mid-implementation.
|
|
14
|
+
|
|
15
|
+
If context usage exceeds 60% at any point during this skill, stop and tell the user to continue in a new conversation, referencing this plan file.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
8
19
|
## Process
|
|
9
20
|
|
|
10
21
|
### 1. Requirements First
|
|
@@ -13,7 +24,15 @@ description: Scaffold a new feature following TDD and project conventions. Use w
|
|
|
13
24
|
- **If no spec exists, call @business-intelligence** to run the discovery process and produce a functional issue in `.requirements/<feature-name>.md`
|
|
14
25
|
- Wait for the functional issue to be written and confirmed by the user before proceeding to Step 2
|
|
15
26
|
|
|
16
|
-
### 2.
|
|
27
|
+
### 2. Read the Architecture Snapshot
|
|
28
|
+
|
|
29
|
+
Read `.cursor/memory/architecture-snapshot.md`. This tells you:
|
|
30
|
+
- Which shadcn/ui components are already installed (no need to `shadcn add` duplicates)
|
|
31
|
+
- The current DB schema (what tables exist, what to extend)
|
|
32
|
+
- Existing features and their paths (avoid recreating patterns)
|
|
33
|
+
- Canonical pattern references (copy from these, do not reinvent)
|
|
34
|
+
|
|
35
|
+
### 3. Create Feature Structure
|
|
17
36
|
```
|
|
18
37
|
src/features/<feature-name>/
|
|
19
38
|
├── components/
|
|
@@ -25,7 +44,7 @@ src/features/<feature-name>/
|
|
|
25
44
|
└── __tests__/
|
|
26
45
|
```
|
|
27
46
|
|
|
28
|
-
###
|
|
47
|
+
### 4. Pre-Test Setup (do this before writing any tests)
|
|
29
48
|
|
|
30
49
|
Update `vitest.config.ts` to exclude files that cannot be meaningfully tested in jsdom:
|
|
31
50
|
- Drizzle schema file(s) (`src/shared/db/schema.ts`)
|
|
@@ -42,32 +61,76 @@ exclude: [
|
|
|
42
61
|
|
|
43
62
|
Configuring exclusions upfront prevents reactive coverage fixes mid-implementation.
|
|
44
63
|
|
|
45
|
-
###
|
|
46
|
-
|
|
64
|
+
### 5. TDD: RED Phase
|
|
65
|
+
|
|
66
|
+
Write ALL test files first — zero implementation code at this stage:
|
|
47
67
|
- `__tests__/<component>.test.tsx` — component tests
|
|
48
|
-
- `__tests__/use-<feature>.test.ts` — hook tests
|
|
49
|
-
- `__tests__/<action>.test.ts` — action tests (see `references/server-action-test-template.md`)
|
|
68
|
+
- `__tests__/use-<feature>.test.ts` — hook tests (if applicable)
|
|
69
|
+
- `__tests__/<action>.action.test.ts` — action tests (see `references/server-action-test-template.md`)
|
|
50
70
|
- `__tests__/<feature>.queries.test.ts` — query tests
|
|
51
71
|
|
|
52
|
-
|
|
72
|
+
Import mock helpers from `@/shared/test-utils/supabase-mock` — do not inline mock chains.
|
|
73
|
+
|
|
74
|
+
**BLOCKING GATE — do not proceed until this passes:**
|
|
75
|
+
Run `pnpm test:unit` and paste the output here. All new tests must **FAIL** (red). If any new test passes without implementation, the test is wrong — fix it before continuing.
|
|
76
|
+
|
|
77
|
+
### 6. TDD: GREEN Phase
|
|
53
78
|
|
|
54
|
-
|
|
55
|
-
Implement minimum code to pass each test:
|
|
56
|
-
- Components → actions → hooks → API routes
|
|
79
|
+
Launch **@backend and @frontend in parallel** — they work on independent files:
|
|
57
80
|
|
|
58
|
-
|
|
81
|
+
| @backend handles | @frontend handles |
|
|
82
|
+
|-----------------|-------------------|
|
|
83
|
+
| `actions/` | `components/` |
|
|
84
|
+
| `queries/` | `page.tsx` |
|
|
85
|
+
| `schema.ts` changes | shadcn component installs |
|
|
86
|
+
| RLS policies | Loading/empty states |
|
|
59
87
|
|
|
60
|
-
|
|
88
|
+
Do NOT serialize these — they touch different files.
|
|
89
|
+
|
|
90
|
+
**BLOCKING GATE — do not proceed until this passes:**
|
|
91
|
+
Run `pnpm test:unit` and paste the output. All tests must **PASS** (green).
|
|
92
|
+
|
|
93
|
+
### 7. Refactor
|
|
61
94
|
Clean up while keeping tests green.
|
|
62
95
|
|
|
63
|
-
###
|
|
96
|
+
### 8. Verify & Self-Check
|
|
97
|
+
|
|
98
|
+
Run all three and paste output:
|
|
64
99
|
```bash
|
|
65
|
-
pnpm test:coverage # Must show 100%
|
|
100
|
+
pnpm test:coverage # Must show 100% across all metrics
|
|
66
101
|
pnpm lint # Must pass with 0 warnings
|
|
67
102
|
pnpm typecheck # Must pass with 0 errors
|
|
68
103
|
```
|
|
69
104
|
|
|
105
|
+
Before moving to the review gate, verify manually:
|
|
106
|
+
- [ ] No `any` types: `grep -r ": any" src/features/<feature>/`
|
|
107
|
+
- [ ] No `eslint-disable` comments
|
|
108
|
+
- [ ] All UI text in Spanish, all `it()`/`describe()` text in English
|
|
109
|
+
- [ ] No file over 200 lines
|
|
110
|
+
- [ ] No function over 20 lines
|
|
111
|
+
- [ ] AAA pattern (`// Arrange`, `// Act`, `// Assert`) in every test
|
|
112
|
+
- [ ] `ActionResult` returned from every Server Action mutation
|
|
113
|
+
- [ ] Toast feedback (`toast.success` / `toast.error`) for every mutation
|
|
114
|
+
- [ ] RLS policies written for every new table
|
|
115
|
+
|
|
116
|
+
### 9. Review Gate
|
|
117
|
+
|
|
118
|
+
Run @code-reviewer and @security-researcher **in parallel** on the changed files. Fix any findings before marking the feature complete.
|
|
119
|
+
|
|
120
|
+
This step is not optional. The feature is not done until both reviewers pass.
|
|
121
|
+
|
|
122
|
+
### 10. Update Architecture Snapshot
|
|
123
|
+
|
|
124
|
+
Update `.cursor/memory/architecture-snapshot.md`:
|
|
125
|
+
- Add new DB tables to the schema table
|
|
126
|
+
- Add new shadcn components to the installed list
|
|
127
|
+
- Add the new feature to the Existing Features table
|
|
128
|
+
- Add any new canonical pattern references that differ from existing ones
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
70
132
|
## Guardrails
|
|
71
|
-
- NEVER start implementation before
|
|
133
|
+
- NEVER start implementation before the RED gate is confirmed with actual test output
|
|
72
134
|
- 100% coverage before marking done
|
|
73
135
|
- Follow `features/* → shared/*` dependency direction
|
|
136
|
+
- RLS is mandatory for every new table — not a post-step
|
|
@@ -2,50 +2,102 @@
|
|
|
2
2
|
|
|
3
3
|
Test Server Actions by mocking `createClient`, asserting on the returned `ActionResult`, and verifying side effects.
|
|
4
4
|
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
5
7
|
```typescript
|
|
6
|
-
import { vi, it, expect, describe } from "vitest";
|
|
8
|
+
import { vi, it, expect, describe, beforeEach } from "vitest";
|
|
9
|
+
import { createClient } from "@/shared/lib/supabase/server";
|
|
10
|
+
import { revalidatePath } from "next/cache";
|
|
11
|
+
import { makeChain, makeSupabaseMock } from "@/shared/test-utils/supabase-mock";
|
|
7
12
|
import { myAction } from "@/features/example/actions/my.action";
|
|
8
13
|
|
|
9
|
-
vi.mock("@/shared/lib/supabase/server", () => ({
|
|
10
|
-
createClient: vi.fn(),
|
|
11
|
-
}));
|
|
14
|
+
vi.mock("@/shared/lib/supabase/server", () => ({ createClient: vi.fn() }));
|
|
12
15
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const mockSupabase = {
|
|
35
|
-
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: mockUser } }) },
|
|
36
|
-
from: vi.fn().mockReturnThis(),
|
|
37
|
-
insert: vi.fn().mockResolvedValue({ error: null }),
|
|
38
|
-
};
|
|
39
|
-
vi.mocked(createClient).mockResolvedValue(mockSupabase as never);
|
|
40
|
-
const formData = new FormData();
|
|
41
|
-
formData.set("name", "Test Item");
|
|
42
|
-
|
|
43
|
-
// Act
|
|
44
|
-
const result = await myAction(formData);
|
|
45
|
-
|
|
46
|
-
// Assert
|
|
47
|
-
expect(result).toEqual({ status: "success", message: expect.any(String) });
|
|
48
|
-
expect(revalidatePath).toHaveBeenCalledWith("/");
|
|
49
|
-
});
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Unauthenticated case (required in every action test)
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
it("returns error when user is not authenticated", async () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
const { mockClient } = makeSupabaseMock({ user: null });
|
|
28
|
+
vi.mocked(createClient).mockResolvedValue(mockClient as never);
|
|
29
|
+
const formData = new FormData();
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const result = await myAction(formData);
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(result).toEqual({ status: "error", message: "No autenticado" });
|
|
36
|
+
expect(revalidatePath).not.toHaveBeenCalled();
|
|
50
37
|
});
|
|
51
38
|
```
|
|
39
|
+
|
|
40
|
+
## Happy path (insert example)
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
it("returns success and revalidates on valid input", async () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
const { mockClient, mockFrom } = makeSupabaseMock({ user: { id: "user-1" } });
|
|
46
|
+
vi.mocked(createClient).mockResolvedValue(mockClient as never);
|
|
47
|
+
mockFrom.mockReturnValue(makeChain({ error: null }));
|
|
48
|
+
const formData = new FormData();
|
|
49
|
+
formData.set("title", "My item");
|
|
50
|
+
|
|
51
|
+
// Act
|
|
52
|
+
const result = await myAction(formData);
|
|
53
|
+
|
|
54
|
+
// Assert
|
|
55
|
+
expect(result).toEqual({ status: "success", message: expect.any(String) });
|
|
56
|
+
expect(revalidatePath).toHaveBeenCalledWith("/");
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## DB error case
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
it("returns error when db insert fails", async () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const { mockClient, mockFrom } = makeSupabaseMock({ user: { id: "user-1" } });
|
|
66
|
+
vi.mocked(createClient).mockResolvedValue(mockClient as never);
|
|
67
|
+
mockFrom.mockReturnValue(makeChain({ error: { message: "db error" } }));
|
|
68
|
+
const formData = new FormData();
|
|
69
|
+
formData.set("title", "My item");
|
|
70
|
+
|
|
71
|
+
// Act
|
|
72
|
+
const result = await myAction(formData);
|
|
73
|
+
|
|
74
|
+
// Assert
|
|
75
|
+
expect(result).toEqual({ status: "error", message: expect.any(String) });
|
|
76
|
+
expect(revalidatePath).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Validation error case
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
it("returns error when required field is missing", async () => {
|
|
84
|
+
// Arrange
|
|
85
|
+
const { mockClient } = makeSupabaseMock({ user: { id: "user-1" } });
|
|
86
|
+
vi.mocked(createClient).mockResolvedValue(mockClient as never);
|
|
87
|
+
const formData = new FormData(); // no fields set
|
|
88
|
+
|
|
89
|
+
// Act
|
|
90
|
+
const result = await myAction(formData);
|
|
91
|
+
|
|
92
|
+
// Assert
|
|
93
|
+
expect(result).toEqual({ status: "error", message: expect.any(String) });
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Key Rules
|
|
98
|
+
|
|
99
|
+
- Always import `makeChain` and `makeSupabaseMock` from `@/shared/test-utils/supabase-mock`
|
|
100
|
+
- Every action test must cover: unauthenticated, validation error, DB error, happy path
|
|
101
|
+
- `mockFrom` returns a chain — each chained method (`.select()`, `.insert()`, etc.) returns the same chain object
|
|
102
|
+
- `then` on the chain resolves with the value you pass to `makeChain()`
|
|
103
|
+
- Never use `mockReturnThis()` — it does not match Supabase's actual chaining API
|
package/template/CLAUDE.md
CHANGED
|
@@ -46,4 +46,14 @@ The `/create-feature` skill reads the requirements file directly, so the BI conv
|
|
|
46
46
|
- Components: PascalCase `.tsx`
|
|
47
47
|
- Tests: colocated in `__tests__/`, named `name.test.ts`
|
|
48
48
|
|
|
49
|
+
# MCP Memory Service
|
|
50
|
+
|
|
51
|
+
This project uses [mcp-memory-service](https://github.com/doobidoo/mcp-memory-service) for persistent semantic memory across sessions.
|
|
52
|
+
Config is shared via `.cursor/mcp.json` (symlinked to `.mcp.json`).
|
|
53
|
+
|
|
54
|
+
If not installed automatically during scaffolding:
|
|
55
|
+
```bash
|
|
56
|
+
pip install mcp-memory-service
|
|
57
|
+
```
|
|
58
|
+
|
|
49
59
|
# Maintenance Notes
|
|
@@ -21,7 +21,7 @@ export default async function HomePage() {
|
|
|
21
21
|
<form action={logoutAction}>
|
|
22
22
|
<button
|
|
23
23
|
type="submit"
|
|
24
|
-
className="rounded bg-
|
|
24
|
+
className="rounded bg-muted px-4 py-2 text-sm hover:bg-muted-foreground/10"
|
|
25
25
|
>
|
|
26
26
|
Cerrar sesión
|
|
27
27
|
</button>
|
|
@@ -34,7 +34,7 @@ export function AddTodoForm() {
|
|
|
34
34
|
placeholder="Nueva tarea…"
|
|
35
35
|
required
|
|
36
36
|
disabled={isPending}
|
|
37
|
-
className="flex-1 rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-
|
|
37
|
+
className="flex-1 rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
38
38
|
aria-label="Nueva tarea"
|
|
39
39
|
/>
|
|
40
40
|
<Button type="submit" loading={isPending}>
|
|
@@ -17,7 +17,7 @@ export function TodoList({ items }: TodoListProps) {
|
|
|
17
17
|
|
|
18
18
|
if (items.length === 0) {
|
|
19
19
|
return (
|
|
20
|
-
<p className="mt-4 text-center text-sm text-
|
|
20
|
+
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
21
21
|
No tienes tareas. ¡Agrega una!
|
|
22
22
|
</p>
|
|
23
23
|
);
|
|
@@ -44,10 +44,10 @@ export function TodoList({ items }: TodoListProps) {
|
|
|
44
44
|
}}
|
|
45
45
|
aria-label={`Marcar "${todo.title}" como completada`}
|
|
46
46
|
/>
|
|
47
|
-
<span className={todo.completed ? "line-through text-
|
|
47
|
+
<span className={todo.completed ? "line-through text-muted-foreground" : ""}>
|
|
48
48
|
{todo.title}
|
|
49
49
|
</span>
|
|
50
|
-
{isPending && <Spinner className="size-4 text-
|
|
50
|
+
{isPending && <Spinner className="size-4 text-muted-foreground" />}
|
|
51
51
|
</label>
|
|
52
52
|
<button
|
|
53
53
|
type="button"
|
|
@@ -59,7 +59,7 @@ export function TodoList({ items }: TodoListProps) {
|
|
|
59
59
|
else toast.success(result.message);
|
|
60
60
|
});
|
|
61
61
|
}}
|
|
62
|
-
className="rounded px-2 py-1 text-sm text-
|
|
62
|
+
className="rounded px-2 py-1 text-sm text-destructive hover:bg-destructive/10"
|
|
63
63
|
aria-label={`Eliminar "${todo.title}"`}
|
|
64
64
|
>
|
|
65
65
|
Eliminar
|
|
@@ -19,7 +19,16 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
19
19
|
return (
|
|
20
20
|
<QueryClientProvider client={queryClient}>
|
|
21
21
|
{children}
|
|
22
|
-
<Toaster
|
|
22
|
+
<Toaster
|
|
23
|
+
toastOptions={{
|
|
24
|
+
classNames: {
|
|
25
|
+
success: "!bg-success !text-success-foreground !border-success",
|
|
26
|
+
error: "!bg-destructive !text-destructive-foreground !border-destructive",
|
|
27
|
+
warning: "!bg-warning !text-warning-foreground !border-warning",
|
|
28
|
+
info: "!bg-info !text-info-foreground !border-info",
|
|
29
|
+
},
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
23
32
|
</QueryClientProvider>
|
|
24
33
|
);
|
|
25
34
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
type Chain = Record<string, ReturnType<typeof vi.fn>>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a chainable Supabase query mock.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const chain = makeChain({ data: [...], error: null });
|
|
10
|
+
* mockSupabase.from.mockReturnValue(chain);
|
|
11
|
+
*
|
|
12
|
+
* Supports: .select() .insert() .update() .delete() .eq() .single() .order()
|
|
13
|
+
* Each method returns the same chain, so calls can be chained arbitrarily.
|
|
14
|
+
* The chain resolves with `resolvedValue` when awaited.
|
|
15
|
+
*/
|
|
16
|
+
export function makeChain(resolvedValue: unknown): Chain {
|
|
17
|
+
const chain: Chain = {};
|
|
18
|
+
const methods = [
|
|
19
|
+
"select",
|
|
20
|
+
"insert",
|
|
21
|
+
"update",
|
|
22
|
+
"delete",
|
|
23
|
+
"eq",
|
|
24
|
+
"single",
|
|
25
|
+
"order",
|
|
26
|
+
"limit",
|
|
27
|
+
"match",
|
|
28
|
+
];
|
|
29
|
+
for (const method of methods) {
|
|
30
|
+
chain[method] = vi.fn().mockReturnValue(chain);
|
|
31
|
+
}
|
|
32
|
+
chain["then"] = vi.fn((cb: (v: unknown) => unknown) => cb(resolvedValue));
|
|
33
|
+
return chain;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a minimal Supabase client mock with auth + from.
|
|
38
|
+
*
|
|
39
|
+
* Usage:
|
|
40
|
+
* const { mockClient, mockFrom } = makeSupabaseMock({ user: mockUser });
|
|
41
|
+
* vi.mocked(createClient).mockResolvedValue(mockClient as never);
|
|
42
|
+
* mockFrom.mockReturnValue(makeChain({ data: [...], error: null }));
|
|
43
|
+
*/
|
|
44
|
+
export function makeSupabaseMock(authResult: {
|
|
45
|
+
user: { id: string } | null;
|
|
46
|
+
error?: unknown;
|
|
47
|
+
}) {
|
|
48
|
+
const mockFrom = vi.fn();
|
|
49
|
+
const mockClient = {
|
|
50
|
+
auth: {
|
|
51
|
+
getUser: vi.fn().mockResolvedValue({
|
|
52
|
+
data: { user: authResult.user },
|
|
53
|
+
error: authResult.error ?? null,
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
from: mockFrom,
|
|
57
|
+
};
|
|
58
|
+
return { mockClient, mockFrom };
|
|
59
|
+
}
|
package/template/tailwind.css
CHANGED
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
--color-accent-foreground: #ffffff;
|
|
14
14
|
--color-destructive: hsl(0 84.2% 60.2%);
|
|
15
15
|
--color-destructive-foreground: #ffffff;
|
|
16
|
+
--color-success: hsl(142 71% 45%);
|
|
17
|
+
--color-success-foreground: #ffffff;
|
|
18
|
+
--color-warning: hsl(38 92% 50%);
|
|
19
|
+
--color-warning-foreground: #ffffff;
|
|
20
|
+
--color-info: hsl(199 89% 48%);
|
|
21
|
+
--color-info-foreground: #ffffff;
|
|
16
22
|
--color-border: #e5e5e5;
|
|
17
23
|
--color-input: #e5e5e5;
|
|
18
24
|
--color-ring: #022633;
|