@gallopsystems/agent-skills 1.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.
Files changed (52) hide show
  1. package/README.md +137 -0
  2. package/package.json +26 -0
  3. package/plugins/doctl/.claude-plugin/plugin.json +8 -0
  4. package/plugins/doctl/skills/doctl/SKILL.md +93 -0
  5. package/plugins/kysely-postgres/.claude-plugin/plugin.json +8 -0
  6. package/plugins/kysely-postgres/skills/kysely-postgres/SKILL.md +1101 -0
  7. package/plugins/kysely-postgres/skills/kysely-postgres/references/aggregations.ts +167 -0
  8. package/plugins/kysely-postgres/skills/kysely-postgres/references/ctes.ts +165 -0
  9. package/plugins/kysely-postgres/skills/kysely-postgres/references/expressions.ts +272 -0
  10. package/plugins/kysely-postgres/skills/kysely-postgres/references/joins.ts +206 -0
  11. package/plugins/kysely-postgres/skills/kysely-postgres/references/json-arrays.ts +398 -0
  12. package/plugins/kysely-postgres/skills/kysely-postgres/references/mutations.ts +199 -0
  13. package/plugins/kysely-postgres/skills/kysely-postgres/references/orderby-pagination.ts +117 -0
  14. package/plugins/kysely-postgres/skills/kysely-postgres/references/relations.ts +176 -0
  15. package/plugins/kysely-postgres/skills/kysely-postgres/references/select-where.ts +146 -0
  16. package/plugins/linear/.claude-plugin/plugin.json +8 -0
  17. package/plugins/linear/skills/linear/SKILL.md +1040 -0
  18. package/plugins/linear/skills/linear/bin/linear.mjs +1228 -0
  19. package/plugins/linear/skills/linear/tech-stack.md +273 -0
  20. package/plugins/nitro-testing/.claude-plugin/plugin.json +8 -0
  21. package/plugins/nitro-testing/skills/nitro-testing/SKILL.md +497 -0
  22. package/plugins/nitro-testing/skills/nitro-testing/async-testing.md +270 -0
  23. package/plugins/nitro-testing/skills/nitro-testing/ci-setup.md +226 -0
  24. package/plugins/nitro-testing/skills/nitro-testing/examples/global-setup.ts +90 -0
  25. package/plugins/nitro-testing/skills/nitro-testing/examples/handler.test.ts +167 -0
  26. package/plugins/nitro-testing/skills/nitro-testing/examples/setup.ts +29 -0
  27. package/plugins/nitro-testing/skills/nitro-testing/examples/test-utils-index.ts +297 -0
  28. package/plugins/nitro-testing/skills/nitro-testing/examples/vitest.config.ts +42 -0
  29. package/plugins/nitro-testing/skills/nitro-testing/factories.md +278 -0
  30. package/plugins/nitro-testing/skills/nitro-testing/frontend-testing.md +512 -0
  31. package/plugins/nitro-testing/skills/nitro-testing/test-utils.md +262 -0
  32. package/plugins/nitro-testing/skills/nitro-testing/transaction-rollback.md +183 -0
  33. package/plugins/nitro-testing/skills/nitro-testing/vitest-config.md +236 -0
  34. package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +8 -0
  35. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +260 -0
  36. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +228 -0
  37. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +174 -0
  38. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/deep-linking.md +190 -0
  39. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-middleware.ts +32 -0
  40. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-utils.ts +51 -0
  41. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/deep-link-page.vue +61 -0
  42. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/service-util.ts +63 -0
  43. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/sse-endpoint.ts +59 -0
  44. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/validation-endpoint.ts +38 -0
  45. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +178 -0
  46. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/nitro-tasks.md +243 -0
  47. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +162 -0
  48. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-services.md +238 -0
  49. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/sse.md +221 -0
  50. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +166 -0
  51. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/validation.md +131 -0
  52. package/scripts/link-skills.mjs +252 -0
@@ -0,0 +1,262 @@
1
+ # Test Utilities
2
+
3
+ > **Example:** [test-utils-index.ts](./examples/test-utils-index.ts)
4
+
5
+ Mock event creators, global stubs, and assertion helpers for testing Nitro handlers.
6
+
7
+ ## Mock Event Helpers
8
+
9
+ Create H3 events for testing handlers:
10
+
11
+ ```typescript
12
+ import { createEvent } from "h3";
13
+ import { IncomingMessage, ServerResponse } from "http";
14
+ import { Socket } from "net";
15
+
16
+ type Params = Record<string, string | number>;
17
+
18
+ export function createMockEvent(options: {
19
+ method?: string;
20
+ params?: Params;
21
+ body?: unknown;
22
+ query?: Record<string, string>;
23
+ }) {
24
+ const { method = "GET", params = {}, body, query = {} } = options;
25
+
26
+ const socket = new Socket();
27
+ const req = new IncomingMessage(socket);
28
+ req.method = method;
29
+ req.url = "/" + (Object.keys(query).length
30
+ ? "?" + new URLSearchParams(query).toString()
31
+ : "");
32
+
33
+ const res = new ServerResponse(req);
34
+ const event = createEvent(req, res);
35
+
36
+ // Set route params
37
+ event.context.params = Object.fromEntries(
38
+ Object.entries(params).map(([k, v]) => [k, String(v)])
39
+ );
40
+
41
+ // Set mock body and query for validation functions
42
+ if (body !== undefined) {
43
+ event.context._mockBody = body;
44
+ }
45
+ if (Object.keys(query).length > 0) {
46
+ event.context._mockQuery = query;
47
+ }
48
+
49
+ return event;
50
+ }
51
+
52
+ // Convenience helpers
53
+ export function mockGet(params: Params, query?: Record<string, string>) {
54
+ return createMockEvent({ method: "GET", params, query });
55
+ }
56
+
57
+ export function mockPost(params: Params, body: unknown) {
58
+ return createMockEvent({ method: "POST", params, body });
59
+ }
60
+
61
+ export function mockPatch(params: Params, body: unknown) {
62
+ return createMockEvent({ method: "PATCH", params, body });
63
+ }
64
+
65
+ export function mockDelete(params: Params) {
66
+ return createMockEvent({ method: "DELETE", params });
67
+ }
68
+ ```
69
+
70
+ ### Usage
71
+
72
+ ```typescript
73
+ // GET /api/users/123?include=profile
74
+ const event = mockGet({ id: 123 }, { include: "profile" });
75
+
76
+ // POST /api/users with JSON body
77
+ const event = mockPost({}, { email: "test@example.com", name: "Test" });
78
+
79
+ // PATCH /api/users/123 with partial update
80
+ const event = mockPatch({ id: 123 }, { status: "active" });
81
+
82
+ // DELETE /api/users/123
83
+ const event = mockDelete({ id: 123 });
84
+ ```
85
+
86
+ ## Global Stubs
87
+
88
+ Stub Nuxt auto-imports so handlers work in test environment:
89
+
90
+ ```typescript
91
+ import { vi } from "vitest";
92
+
93
+ export async function setupHandlerMocks() {
94
+ // Unwrap defineEventHandler
95
+ vi.stubGlobal("defineEventHandler", (handler: Function) => handler);
96
+
97
+ // Default test user
98
+ vi.stubGlobal("getUserSession", async () => ({
99
+ user: { id: 1, firstName: "Test", lastName: "User", role: "admin" },
100
+ }));
101
+
102
+ vi.stubGlobal("setUserSession", async () => {});
103
+
104
+ // Database access (currentTrx set by test fixture)
105
+ vi.stubGlobal("useDatabase", () => {
106
+ if (!currentTrx) {
107
+ throw new Error("useDatabase called outside of test transaction");
108
+ }
109
+ return currentTrx;
110
+ });
111
+
112
+ // Error helper
113
+ vi.stubGlobal("createError", (opts: {
114
+ statusCode: number;
115
+ message?: string;
116
+ statusMessage?: string;
117
+ data?: unknown;
118
+ }) => {
119
+ const error = new Error(opts.message || opts.statusMessage || "") as any;
120
+ error.statusCode = opts.statusCode;
121
+ if (opts.data !== undefined) error.data = opts.data;
122
+ return error;
123
+ });
124
+
125
+ // Route params
126
+ vi.stubGlobal("getRouterParam", (event: any, param: string) => {
127
+ return event.context.params?.[param];
128
+ });
129
+
130
+ vi.stubGlobal("getValidatedRouterParams", async (event: any, validate: Function) => {
131
+ return validate(event.context.params ?? {});
132
+ });
133
+
134
+ // Query params
135
+ vi.stubGlobal("getQuery", (event: any) => {
136
+ return event.context._mockQuery ?? {};
137
+ });
138
+
139
+ vi.stubGlobal("getValidatedQuery", async (event: any, validate: Function) => {
140
+ return validate(event.context._mockQuery ?? {});
141
+ });
142
+
143
+ // Request body
144
+ vi.stubGlobal("readBody", async (event: any) => event.context._mockBody);
145
+
146
+ vi.stubGlobal("readValidatedBody", async (event: any, validate: Function) => {
147
+ return validate(event.context._mockBody ?? {});
148
+ });
149
+ }
150
+ ```
151
+
152
+ ## Assertion Helpers
153
+
154
+ ### expectHttpError
155
+
156
+ Test that handlers throw proper HTTP errors:
157
+
158
+ ```typescript
159
+ export async function expectHttpError(
160
+ promise: Promise<unknown>,
161
+ expected: { statusCode: number; message?: string }
162
+ ) {
163
+ await expect(promise).rejects.toMatchObject(expected);
164
+ }
165
+ ```
166
+
167
+ ### Usage
168
+
169
+ ```typescript
170
+ test("returns 404 for non-existent user", async ({ factories: _ }) => {
171
+ const event = mockGet({ id: 999999 });
172
+
173
+ await expectHttpError(handler(event), {
174
+ statusCode: 404,
175
+ message: "User not found",
176
+ });
177
+ });
178
+
179
+ test("returns 400 for invalid input", async ({ factories: _ }) => {
180
+ const event = mockPost({}, { invalidField: "bad" });
181
+
182
+ await expectHttpError(handler(event), { statusCode: 400 });
183
+ });
184
+
185
+ test("returns 401 for unauthenticated request", async ({ factories: _ }) => {
186
+ // Override default session
187
+ vi.mocked(getUserSession).mockResolvedValueOnce({ user: null });
188
+
189
+ const event = mockGet({});
190
+ await expectHttpError(handler(event), { statusCode: 401 });
191
+ });
192
+ ```
193
+
194
+ ## Customizing Session Per Test
195
+
196
+ Override the default test user:
197
+
198
+ ```typescript
199
+ import { vi } from "vitest";
200
+
201
+ test("non-admin cannot delete", async ({ factories }) => {
202
+ // Override session for this test
203
+ vi.stubGlobal("getUserSession", async () => ({
204
+ user: { id: 2, role: "user" }, // Not admin
205
+ }));
206
+
207
+ const item = await factories.item();
208
+ const event = mockDelete({ id: item.id });
209
+
210
+ await expectHttpError(handler(event), { statusCode: 403 });
211
+ });
212
+ ```
213
+
214
+ Or use `vi.mocked` for one-off overrides:
215
+
216
+ ```typescript
217
+ test("user can only see own items", async ({ factories }) => {
218
+ const owner = await factories.user();
219
+ const other = await factories.user();
220
+ const item = await factories.item({ ownerId: owner.id });
221
+
222
+ // Mock as the other user
223
+ vi.mocked(getUserSession).mockResolvedValueOnce({
224
+ user: { id: other.id, role: "user" },
225
+ });
226
+
227
+ const event = mockGet({ id: item.id });
228
+ await expectHttpError(handler(event), { statusCode: 404 });
229
+ });
230
+ ```
231
+
232
+ ## Re-exports for Convenience
233
+
234
+ Export everything from one place:
235
+
236
+ ```typescript
237
+ // server/test-utils/index.ts
238
+ export { describe, expect, beforeAll } from "vitest";
239
+ export { test } from "./fixtures"; // Custom fixture
240
+ export {
241
+ createMockEvent,
242
+ mockGet,
243
+ mockPost,
244
+ mockPatch,
245
+ mockDelete,
246
+ expectHttpError,
247
+ } from "./helpers";
248
+ export { setupHandlerMocks } from "./stubs";
249
+ export type { Factories } from "./factories";
250
+ ```
251
+
252
+ Then in tests:
253
+
254
+ ```typescript
255
+ import {
256
+ describe,
257
+ test,
258
+ expect,
259
+ mockPost,
260
+ expectHttpError,
261
+ } from "~/server/test-utils";
262
+ ```
@@ -0,0 +1,183 @@
1
+ # Transaction Rollback Pattern
2
+
3
+ > **Example:** [test-utils-index.ts](./examples/test-utils-index.ts)
4
+
5
+ The core isolation pattern: each test runs inside a database transaction that auto-rolls back.
6
+
7
+ ## Why This Pattern?
8
+
9
+ | Approach | Speed | Isolation | Real SQL |
10
+ |----------|-------|-----------|----------|
11
+ | Truncate tables | Slow | ✅ | ✅ |
12
+ | Mock database | Fast | ✅ | ❌ |
13
+ | **Transaction rollback** | **Fast** | **✅** | **✅** |
14
+
15
+ Transaction rollback gives you real SQL testing with mock-like speed.
16
+
17
+ ## Implementation
18
+
19
+ ### Custom Vitest Fixture
20
+
21
+ ```typescript
22
+ import { vi, test as base, expect } from "vitest";
23
+ import { db } from "../utils/db";
24
+ import type { Transaction } from "kysely";
25
+ import type { DB } from "../db/db";
26
+
27
+ // Current test transaction - handlers access via stubbed useDatabase
28
+ let currentTrx: Transaction<DB> | null = null;
29
+
30
+ interface TestFixtures {
31
+ factories: Factories;
32
+ db: Transaction<DB>;
33
+ }
34
+
35
+ export const test = base.extend<TestFixtures>({
36
+ // The factories fixture sets up the transaction
37
+ factories: async ({}, use) => {
38
+ await db.transaction().execute(async (trx) => {
39
+ currentTrx = trx;
40
+ try {
41
+ await use(createFactories(trx));
42
+ // Force rollback by throwing
43
+ throw { __rollback: true };
44
+ } finally {
45
+ currentTrx = null;
46
+ }
47
+ }).catch((e) => {
48
+ // Swallow our rollback signal
49
+ if (e && typeof e === "object" && "__rollback" in e) {
50
+ return;
51
+ }
52
+ throw e;
53
+ });
54
+ },
55
+
56
+ // The db fixture exposes the transaction for direct queries
57
+ db: async ({ factories: _ }, use) => {
58
+ if (!currentTrx) {
59
+ throw new Error("db fixture used outside transaction context");
60
+ }
61
+ await use(currentTrx);
62
+ },
63
+ });
64
+
65
+ export { describe, expect, beforeAll } from "vitest";
66
+ ```
67
+
68
+ ### Stubbing useDatabase
69
+
70
+ ```typescript
71
+ // In setup.ts or as part of setupHandlerMocks()
72
+ vi.stubGlobal("useDatabase", () => {
73
+ if (!currentTrx) {
74
+ throw new Error("useDatabase called outside of test transaction");
75
+ }
76
+ return currentTrx;
77
+ });
78
+ ```
79
+
80
+ Now any handler code that calls `useDatabase()` gets the test transaction.
81
+
82
+ ## Handling Nested Transactions
83
+
84
+ Real code often uses `db.transaction()` for atomic operations. Since tests already run in a transaction, we need to handle nested transactions:
85
+
86
+ ```typescript
87
+ // Patch Transaction prototype to handle nesting
88
+ const KyselyModule = await import("kysely");
89
+ const TransactionClass = (KyselyModule as any).Transaction;
90
+
91
+ if (TransactionClass?.prototype) {
92
+ TransactionClass.prototype.transaction = function () {
93
+ const self = this;
94
+ return {
95
+ execute: async <T>(callback: (trx: any) => Promise<T>): Promise<T> => {
96
+ // Just run callback with same transaction (no nesting)
97
+ return callback(self);
98
+ },
99
+ };
100
+ };
101
+ }
102
+ ```
103
+
104
+ This makes code like this work transparently:
105
+
106
+ ```typescript
107
+ // In production: creates real nested transaction
108
+ // In tests: reuses the test transaction
109
+ async function createOrder(data: OrderData) {
110
+ return db.transaction().execute(async (trx) => {
111
+ const order = await trx.insertInto("order").values(data)...;
112
+ await trx.insertInto("order_item").values(...)...;
113
+ return order;
114
+ });
115
+ }
116
+ ```
117
+
118
+ ## Usage Pattern
119
+
120
+ ```typescript
121
+ import { describe, test, expect, mockPost } from "~/server/test-utils";
122
+ import handler from "./index.post";
123
+
124
+ describe("POST /api/orders", () => {
125
+ test("creates order with items", async ({ factories, db }) => {
126
+ // Create test data using factories (transaction-bound)
127
+ const user = await factories.user();
128
+ const product = await factories.product({ price: 100 });
129
+
130
+ // Test the handler
131
+ const event = mockPost({}, {
132
+ userId: user.id,
133
+ items: [{ productId: product.id, quantity: 2 }],
134
+ });
135
+ const result = await handler(event);
136
+
137
+ // Verify in database
138
+ const order = await db
139
+ .selectFrom("order")
140
+ .where("id", "=", result.id)
141
+ .selectAll()
142
+ .executeTakeFirst();
143
+
144
+ expect(order?.total).toBe(200);
145
+
146
+ // Verify order items
147
+ const items = await db
148
+ .selectFrom("order_item")
149
+ .where("order_id", "=", result.id)
150
+ .selectAll()
151
+ .execute();
152
+
153
+ expect(items).toHaveLength(1);
154
+ });
155
+ // Transaction rolls back - database unchanged
156
+ });
157
+ ```
158
+
159
+ ## Key Points
160
+
161
+ 1. **Always destructure `factories`** - Even if unused, it triggers transaction setup
162
+ 2. **Use `db` fixture for assertions** - Not the real db import
163
+ 3. **Nested transactions work** - Thanks to prototype patching
164
+ 4. **No cleanup needed** - Rollback happens automatically
165
+ 5. **Tests are isolated** - Can't affect each other
166
+
167
+ ## Gotcha: Unused Factories
168
+
169
+ Even if you don't create test data, you need the fixture to set up the transaction:
170
+
171
+ ```typescript
172
+ // ❌ Wrong - no transaction, useDatabase will fail
173
+ test("returns empty list", async () => {
174
+ const result = await handler(mockGet({}));
175
+ expect(result).toEqual([]);
176
+ });
177
+
178
+ // ✅ Right - transaction set up via factories fixture
179
+ test("returns empty list", async ({ factories: _ }) => {
180
+ const result = await handler(mockGet({}));
181
+ expect(result).toEqual([]);
182
+ });
183
+ ```
@@ -0,0 +1,236 @@
1
+ # Vitest Configuration
2
+
3
+ > **Example:** [vitest.config.ts](./examples/vitest.config.ts)
4
+
5
+ Configure Vitest for testing Nitro API handlers.
6
+
7
+ ## Basic Configuration
8
+
9
+ ```typescript
10
+ // vitest.config.ts
11
+ import { defineConfig } from "vitest/config";
12
+ import path from "path";
13
+
14
+ export default defineConfig({
15
+ test: {
16
+ // Enable globals (describe, test, expect without imports)
17
+ globals: true,
18
+
19
+ // Node environment (not browser)
20
+ environment: "node",
21
+
22
+ // Run once before all tests - reset DB and run migrations
23
+ globalSetup: ["./server/test-utils/global-setup.ts"],
24
+
25
+ // Run before each test file - set up stubs
26
+ setupFiles: ["./server/test-utils/setup.ts"],
27
+
28
+ // Coverage configuration
29
+ coverage: {
30
+ provider: "v8",
31
+ reporter: ["text", "html", "lcov", "json", "json-summary"],
32
+ include: ["server/**/*.ts"],
33
+ exclude: [
34
+ "server/**/*.test.ts",
35
+ "server/test-utils/**",
36
+ "server/db/migrations/**",
37
+ "server/db/db.d.ts",
38
+ ],
39
+ reportsDirectory: "./coverage",
40
+ },
41
+ },
42
+
43
+ resolve: {
44
+ alias: {
45
+ "~": path.resolve(__dirname),
46
+ "@": path.resolve(__dirname),
47
+ },
48
+ },
49
+ });
50
+ ```
51
+
52
+ ## Global Setup (Runs Once)
53
+
54
+ ```typescript
55
+ // server/test-utils/global-setup.ts
56
+ import { Kysely, PostgresDialect, Migrator, FileMigrationProvider } from "kysely";
57
+ import { Pool } from "pg";
58
+ import path from "path";
59
+ import { promises as fs } from "fs";
60
+
61
+ function getTestConnectionString(): string {
62
+ if (process.env.TEST_POSTGRESQL_CONNECTION_STRING) {
63
+ return process.env.TEST_POSTGRESQL_CONNECTION_STRING;
64
+ }
65
+ return "postgresql://localhost/myapp-test";
66
+ }
67
+
68
+ export async function setup() {
69
+ const connectionString = getTestConnectionString();
70
+ const pool = new Pool({ connectionString });
71
+
72
+ // Check database exists
73
+ try {
74
+ await pool.query("SELECT 1");
75
+ } catch (err: any) {
76
+ if (err.code === "3D000") {
77
+ console.error(`
78
+ ╭─────────────────────────────────────────────────────╮
79
+ │ Error: Test database does not exist. │
80
+ │ │
81
+ │ Run: createdb myapp-test │
82
+ ╰─────────────────────────────────────────────────────╯
83
+ `);
84
+ process.exit(1);
85
+ }
86
+ throw err;
87
+ }
88
+
89
+ // Drop all tables and types for clean slate
90
+ await pool.query(`
91
+ DO $$ DECLARE r RECORD;
92
+ BEGIN
93
+ FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
94
+ EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
95
+ END LOOP;
96
+ FOR r IN (SELECT typname FROM pg_type t
97
+ JOIN pg_namespace n ON t.typnamespace = n.oid
98
+ WHERE n.nspname = 'public' AND t.typtype = 'e') LOOP
99
+ EXECUTE 'DROP TYPE IF EXISTS ' || quote_ident(r.typname) || ' CASCADE';
100
+ END LOOP;
101
+ END $$;
102
+ `);
103
+ await pool.end();
104
+
105
+ // Run migrations
106
+ const db = new Kysely({
107
+ dialect: new PostgresDialect({
108
+ pool: new Pool({ connectionString }),
109
+ }),
110
+ });
111
+
112
+ const migrator = new Migrator({
113
+ db,
114
+ provider: new FileMigrationProvider({
115
+ fs,
116
+ path,
117
+ migrationFolder: path.resolve(__dirname, "../db/migrations"),
118
+ }),
119
+ });
120
+
121
+ const { error, results } = await migrator.migrateToLatest();
122
+
123
+ if (error) {
124
+ console.error("Migration failed:", error);
125
+ throw error;
126
+ }
127
+
128
+ const applied = results?.filter((r) => r.status === "Success") ?? [];
129
+ console.log(`Test DB ready: ${applied.length} migrations applied`);
130
+
131
+ await db.destroy();
132
+ }
133
+ ```
134
+
135
+ ## Setup File (Runs Per Test File)
136
+
137
+ ```typescript
138
+ // server/test-utils/setup.ts
139
+ import { vi } from "vitest";
140
+
141
+ // Get test database connection
142
+ function getTestConnectionString(): string {
143
+ if (process.env.TEST_POSTGRESQL_CONNECTION_STRING) {
144
+ return process.env.TEST_POSTGRESQL_CONNECTION_STRING;
145
+ }
146
+ return "postgresql://localhost/myapp-test";
147
+ }
148
+
149
+ // Stub useRuntimeConfig before anything else
150
+ vi.stubGlobal("useRuntimeConfig", () => ({
151
+ postgresql: {
152
+ connectionString: getTestConnectionString(),
153
+ },
154
+ public: {
155
+ environment: "test",
156
+ },
157
+ }));
158
+
159
+ // Set up handler mocks
160
+ const { setupHandlerMocks } = await import("./index");
161
+ await setupHandlerMocks();
162
+ ```
163
+
164
+ ## Package.json Scripts
165
+
166
+ ```json
167
+ {
168
+ "scripts": {
169
+ "test": "vitest",
170
+ "test:watch": "vitest --watch",
171
+ "test:ui": "vitest --ui",
172
+ "test:coverage": "vitest --coverage",
173
+ "test:run": "vitest run"
174
+ }
175
+ }
176
+ ```
177
+
178
+ ## TypeScript Configuration
179
+
180
+ ```json
181
+ // tsconfig.json - ensure vitest types are included
182
+ {
183
+ "compilerOptions": {
184
+ "types": ["vitest/globals"]
185
+ }
186
+ }
187
+ ```
188
+
189
+ ## Test File Pattern
190
+
191
+ By default, Vitest finds files matching:
192
+ - `**/*.test.ts`
193
+ - `**/*.spec.ts`
194
+
195
+ Co-locate tests with handlers:
196
+
197
+ ```
198
+ server/
199
+ api/
200
+ users/
201
+ index.get.ts # Handler
202
+ index.get.test.ts # Test
203
+ index.post.ts
204
+ index.post.test.ts
205
+ [id].get.ts
206
+ [id].get.test.ts
207
+ ```
208
+
209
+ ## Environment Variables
210
+
211
+ ```bash
212
+ # Local development
213
+ # Uses postgresql://localhost/myapp-test by default
214
+
215
+ # CI/CD
216
+ TEST_POSTGRESQL_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/myapp-test
217
+ ```
218
+
219
+ ## Coverage Thresholds (Optional)
220
+
221
+ ```typescript
222
+ // vitest.config.ts
223
+ export default defineConfig({
224
+ test: {
225
+ coverage: {
226
+ // Fail if coverage drops below thresholds
227
+ thresholds: {
228
+ lines: 80,
229
+ functions: 80,
230
+ branches: 70,
231
+ statements: 80,
232
+ },
233
+ },
234
+ },
235
+ });
236
+ ```
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "nuxt-nitro-api",
3
+ "description": "Nuxt 3 / Nitro API patterns: validation, auth, SSR, tasks, SSE",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "yeedle"
7
+ }
8
+ }