@friggframework/devtools 2.0.0--canary.395.f03dc2b.0 → 2.0.0--canary.395.495dc7d.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.
@@ -0,0 +1,179 @@
1
+ const path = require('path');
2
+ const chalk = require('chalk');
3
+ const dotenv = require('dotenv');
4
+ const {
5
+ validateDatabaseUrl,
6
+ getDatabaseType,
7
+ testDatabaseConnection
8
+ } = require('../utils/database-validator');
9
+ const {
10
+ runPrismaGenerate,
11
+ checkDatabaseState,
12
+ runPrismaMigrate,
13
+ runPrismaDbPush,
14
+ getMigrationCommand
15
+ } = require('../utils/prisma-runner');
16
+ const {
17
+ getDatabaseUrlMissingError,
18
+ getDatabaseTypeNotConfiguredError,
19
+ getDatabaseConnectionError,
20
+ getPrismaCommandError,
21
+ getDatabaseSetupSuccess
22
+ } = require('../utils/error-messages');
23
+
24
+ /**
25
+ * Database Setup Command
26
+ * Sets up the database for a Frigg application:
27
+ * - Validates configuration
28
+ * - Generates Prisma client
29
+ * - Runs migrations (PostgreSQL) or db push (MongoDB)
30
+ */
31
+
32
+ async function dbSetupCommand(options = {}) {
33
+ const verbose = options.verbose || false;
34
+ const stage = options.stage || process.env.STAGE || 'development';
35
+
36
+ console.log(chalk.blue('🔧 Frigg Database Setup'));
37
+ console.log(chalk.gray(`Stage: ${stage}\n`));
38
+
39
+ // Load environment variables from .env file
40
+ const envPath = path.join(process.cwd(), '.env');
41
+ dotenv.config({ path: envPath });
42
+
43
+ try {
44
+ // Step 1: Validate DATABASE_URL
45
+ if (verbose) {
46
+ console.log(chalk.gray('Step 1: Validating DATABASE_URL...'));
47
+ }
48
+
49
+ const urlValidation = validateDatabaseUrl();
50
+ if (!urlValidation.valid) {
51
+ console.error(getDatabaseUrlMissingError());
52
+ process.exit(1);
53
+ }
54
+
55
+ if (verbose) {
56
+ console.log(chalk.green('✓ DATABASE_URL found\n'));
57
+ }
58
+
59
+ // Step 2: Determine database type from app definition
60
+ if (verbose) {
61
+ console.log(chalk.gray('Step 2: Determining database type...'));
62
+ }
63
+
64
+ const dbTypeResult = getDatabaseType();
65
+ if (dbTypeResult.error) {
66
+ console.error(chalk.red('❌ ' + dbTypeResult.error));
67
+ console.error(getDatabaseTypeNotConfiguredError());
68
+ process.exit(1);
69
+ }
70
+
71
+ const dbType = dbTypeResult.dbType;
72
+ console.log(chalk.cyan(`Database type: ${dbType}`));
73
+
74
+ if (verbose) {
75
+ console.log(chalk.green(`✓ Using ${dbType}\n`));
76
+ }
77
+
78
+ // Step 3: Test database connection
79
+ if (verbose) {
80
+ console.log(chalk.gray('Step 3: Testing database connection...'));
81
+ }
82
+
83
+ console.log(chalk.gray('Connecting to database...'));
84
+ const connectionTest = await testDatabaseConnection(urlValidation.url, dbType);
85
+
86
+ if (!connectionTest.connected) {
87
+ console.error(getDatabaseConnectionError(connectionTest.error, dbType));
88
+ process.exit(1);
89
+ }
90
+
91
+ console.log(chalk.green('✓ Database connection verified\n'));
92
+
93
+ // Step 4: Generate Prisma client
94
+ console.log(chalk.cyan('Generating Prisma client...'));
95
+
96
+ const generateResult = await runPrismaGenerate(dbType, verbose);
97
+
98
+ if (!generateResult.success) {
99
+ console.error(getPrismaCommandError('generate', generateResult.error));
100
+ if (generateResult.output) {
101
+ console.error(chalk.gray(generateResult.output));
102
+ }
103
+ process.exit(1);
104
+ }
105
+
106
+ console.log(chalk.green('✓ Prisma client generated\n'));
107
+
108
+ // Step 5: Check database state
109
+ if (verbose) {
110
+ console.log(chalk.gray('Step 5: Checking database state...'));
111
+ }
112
+
113
+ const stateCheck = await checkDatabaseState(dbType);
114
+
115
+ // Step 6: Run migrations or db push
116
+ if (dbType === 'postgresql') {
117
+ console.log(chalk.cyan('Running database migrations...'));
118
+
119
+ const migrationCommand = getMigrationCommand(stage);
120
+
121
+ if (verbose) {
122
+ console.log(chalk.gray(`Using migration command: ${migrationCommand}`));
123
+ }
124
+
125
+ if (stateCheck.upToDate && migrationCommand === 'deploy') {
126
+ console.log(chalk.yellow('Database is already up-to-date'));
127
+ } else {
128
+ const migrateResult = await runPrismaMigrate(migrationCommand, verbose);
129
+
130
+ if (!migrateResult.success) {
131
+ console.error(getPrismaCommandError('migrate', migrateResult.error));
132
+ if (migrateResult.output) {
133
+ console.error(chalk.gray(migrateResult.output));
134
+ }
135
+ process.exit(1);
136
+ }
137
+
138
+ console.log(chalk.green('✓ Migrations applied\n'));
139
+ }
140
+
141
+ } else if (dbType === 'mongodb') {
142
+ console.log(chalk.cyan('Pushing schema to MongoDB...'));
143
+
144
+ const pushResult = await runPrismaDbPush(verbose);
145
+
146
+ if (!pushResult.success) {
147
+ console.error(getPrismaCommandError('db push', pushResult.error));
148
+ if (pushResult.output) {
149
+ console.error(chalk.gray(pushResult.output));
150
+ }
151
+ process.exit(1);
152
+ }
153
+
154
+ console.log(chalk.green('✓ Schema pushed to database\n'));
155
+ }
156
+
157
+ // Success!
158
+ console.log(getDatabaseSetupSuccess(dbType, stage));
159
+
160
+ } catch (error) {
161
+ console.error(chalk.red('\n❌ Database setup failed'));
162
+ console.error(chalk.gray(error.message));
163
+
164
+ if (verbose && error.stack) {
165
+ console.error(chalk.gray('\nStack trace:'));
166
+ console.error(chalk.gray(error.stack));
167
+ }
168
+
169
+ console.error(chalk.yellow('\nTroubleshooting:'));
170
+ console.error(chalk.gray(' • Verify DATABASE_URL in your .env file'));
171
+ console.error(chalk.gray(' • Check database is running and accessible'));
172
+ console.error(chalk.gray(' • Ensure app definition has database configuration'));
173
+ console.error(chalk.gray(' • Run with --verbose flag for more details'));
174
+
175
+ process.exit(1);
176
+ }
177
+ }
178
+
179
+ module.exports = { dbSetupCommand };
@@ -8,6 +8,7 @@ const { buildCommand } = require('./build-command');
8
8
  const { deployCommand } = require('./deploy-command');
9
9
  const { generateIamCommand } = require('./generate-iam-command');
10
10
  const { uiCommand } = require('./ui-command');
11
+ const { dbSetupCommand } = require('./db-setup-command');
11
12
 
12
13
  const program = new Command();
13
14
 
@@ -61,6 +62,13 @@ program
61
62
  .option('--no-open', 'do not open browser automatically')
62
63
  .action(uiCommand);
63
64
 
65
+ program
66
+ .command('db:setup')
67
+ .description('Set up database schema and generate Prisma client')
68
+ .option('-s, --stage <stage>', 'deployment stage', 'development')
69
+ .option('-v, --verbose', 'enable verbose output')
70
+ .action(dbSetupCommand);
71
+
64
72
  program.parse(process.argv);
65
73
 
66
- module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand };
74
+ module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand };
@@ -12,12 +12,14 @@
12
12
  "dependencies": {
13
13
  "@babel/parser": "^7.25.3",
14
14
  "@babel/traverse": "^7.25.3",
15
+ "@friggframework/core": "workspace:*",
15
16
  "@friggframework/schemas": "workspace:*",
16
17
  "@inquirer/prompts": "^5.3.8",
17
18
  "axios": "^1.7.2",
18
19
  "chalk": "^4.1.2",
19
20
  "commander": "^12.1.0",
20
21
  "cross-spawn": "^7.0.3",
22
+ "dotenv": "^16.4.5",
21
23
  "fs-extra": "^11.2.0",
22
24
  "js-yaml": "^4.1.0",
23
25
  "lodash": "4.17.21",
@@ -27,7 +29,8 @@
27
29
  "validate-npm-package-name": "^5.0.0"
28
30
  },
29
31
  "devDependencies": {
30
- "jest": "^29.7.0"
32
+ "jest": "^29.7.0",
33
+ "jest-mock-extended": "^3.0.5"
31
34
  },
32
35
  "keywords": [
33
36
  "frigg",
@@ -1,12 +1,44 @@
1
1
  const { spawn } = require('node:child_process');
2
2
  const path = require('node:path');
3
+ const dotenv = require('dotenv');
4
+ const chalk = require('chalk');
5
+ const {
6
+ validateDatabaseUrl,
7
+ getDatabaseType,
8
+ testDatabaseConnection,
9
+ checkPrismaClientGenerated
10
+ } = require('../utils/database-validator');
11
+ const {
12
+ getDatabaseUrlMissingError,
13
+ getDatabaseTypeNotConfiguredError,
14
+ getDatabaseConnectionError,
15
+ getPrismaClientNotGeneratedError
16
+ } = require('../utils/error-messages');
3
17
 
4
- function startCommand(options) {
18
+ async function startCommand(options) {
5
19
  if (options.verbose) {
6
20
  console.log('Verbose mode enabled');
7
21
  console.log('Options:', options);
8
22
  }
23
+
24
+ console.log(chalk.blue('🚀 Starting Frigg application...\n'));
25
+
26
+ // Load environment variables from .env file
27
+ const envPath = path.join(process.cwd(), '.env');
28
+ dotenv.config({ path: envPath });
29
+
30
+ // Pre-flight database checks
31
+ try {
32
+ await performDatabaseChecks(options.verbose);
33
+ } catch (error) {
34
+ console.error(chalk.red('\n❌ Pre-flight checks failed'));
35
+ console.error(chalk.gray('Fix the issues above before starting the application.\n'));
36
+ process.exit(1);
37
+ }
38
+
39
+ console.log(chalk.green('✓ Database checks passed\n'));
9
40
  console.log('Starting backend and optional frontend...');
41
+
10
42
  // Suppress AWS SDK warning message about maintenance mode
11
43
  process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = 1;
12
44
  // Skip AWS discovery for local development
@@ -53,4 +85,78 @@ function startCommand(options) {
53
85
  });
54
86
  }
55
87
 
88
+ /**
89
+ * Performs pre-flight database validation checks
90
+ * @param {boolean} verbose - Enable verbose output
91
+ * @throws {Error} If any validation check fails
92
+ */
93
+ async function performDatabaseChecks(verbose) {
94
+ // Check 1: Validate DATABASE_URL exists
95
+ if (verbose) {
96
+ console.log(chalk.gray('Checking DATABASE_URL...'));
97
+ }
98
+
99
+ const urlValidation = validateDatabaseUrl();
100
+ if (!urlValidation.valid) {
101
+ console.error(getDatabaseUrlMissingError());
102
+ throw new Error('DATABASE_URL validation failed');
103
+ }
104
+
105
+ if (verbose) {
106
+ console.log(chalk.green('✓ DATABASE_URL found'));
107
+ }
108
+
109
+ // Check 2: Determine database type
110
+ if (verbose) {
111
+ console.log(chalk.gray('Determining database type...'));
112
+ }
113
+
114
+ const dbTypeResult = getDatabaseType();
115
+ if (dbTypeResult.error) {
116
+ console.error(chalk.red('❌ ' + dbTypeResult.error));
117
+ console.error(getDatabaseTypeNotConfiguredError());
118
+ throw new Error('Database type determination failed');
119
+ }
120
+
121
+ const dbType = dbTypeResult.dbType;
122
+
123
+ if (verbose) {
124
+ console.log(chalk.green(`✓ Database type: ${dbType}`));
125
+ }
126
+
127
+ // Check 3: Test database connection
128
+ if (verbose) {
129
+ console.log(chalk.gray('Testing database connection...'));
130
+ }
131
+
132
+ const connectionTest = await testDatabaseConnection(urlValidation.url, dbType, 5000);
133
+
134
+ if (!connectionTest.connected) {
135
+ console.error(getDatabaseConnectionError(connectionTest.error, dbType));
136
+ throw new Error('Database connection failed');
137
+ }
138
+
139
+ if (verbose) {
140
+ console.log(chalk.green('✓ Database connection verified'));
141
+ }
142
+
143
+ // Check 4: Verify Prisma client is generated
144
+ if (verbose) {
145
+ console.log(chalk.gray('Checking Prisma client...'));
146
+ }
147
+
148
+ const clientCheck = checkPrismaClientGenerated(dbType);
149
+
150
+ if (!clientCheck.generated) {
151
+ console.error(getPrismaClientNotGeneratedError(dbType));
152
+ console.error(chalk.yellow('\nRun this command to generate the Prisma client:'));
153
+ console.error(chalk.cyan(' frigg db:setup\n'));
154
+ throw new Error('Prisma client not generated');
155
+ }
156
+
157
+ if (verbose) {
158
+ console.log(chalk.green('✓ Prisma client generated'));
159
+ }
160
+ }
161
+
56
162
  module.exports = { startCommand };
@@ -5,26 +5,56 @@
5
5
  * 1. Sets FRIGG_SKIP_AWS_DISCOVERY=true in the parent process to skip AWS API calls
6
6
  * 2. Suppresses AWS SDK maintenance mode warnings
7
7
  * 3. Spawns serverless with correct configuration
8
+ * 4. Validates database configuration before starting
8
9
  *
9
10
  * This fixes the issue where frigg start would attempt AWS discovery during local development,
10
11
  * causing unnecessary AWS API calls and potential failures when AWS credentials aren't available.
11
12
  */
12
13
 
13
- const { spawn } = require('node:child_process');
14
- const { startCommand } = require('./index');
14
+ // Mock dependencies BEFORE importing startCommand
15
+ const mockValidator = {
16
+ validateDatabaseUrl: jest.fn(),
17
+ getDatabaseType: jest.fn(),
18
+ testDatabaseConnection: jest.fn(),
19
+ checkPrismaClientGenerated: jest.fn()
20
+ };
15
21
 
16
- // Mock the spawn function
17
22
  jest.mock('node:child_process', () => ({
18
23
  spawn: jest.fn(),
19
24
  }));
20
25
 
26
+ jest.mock('../utils/database-validator', () => mockValidator);
27
+ jest.mock('dotenv');
28
+
29
+ const { spawn } = require('node:child_process');
30
+ const { startCommand } = require('./index');
31
+ const { createMockDatabaseValidator } = require('../__tests__/utils/prisma-mock');
32
+ const dotenv = require('dotenv');
33
+
21
34
  describe('startCommand', () => {
22
35
  let mockChildProcess;
36
+ let mockProcessExit;
23
37
 
24
38
  beforeEach(() => {
39
+ // Mock process.exit BEFORE clearAllMocks to prevent actual exits
40
+ mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
41
+
25
42
  // Reset mocks
26
43
  jest.clearAllMocks();
27
44
 
45
+ // Re-apply process.exit mock after clearAllMocks
46
+ mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
47
+
48
+ // Set up default database validator mocks for all tests
49
+ const defaultValidator = createMockDatabaseValidator();
50
+ mockValidator.validateDatabaseUrl.mockImplementation(defaultValidator.validateDatabaseUrl);
51
+ mockValidator.getDatabaseType.mockImplementation(defaultValidator.getDatabaseType);
52
+ mockValidator.testDatabaseConnection.mockImplementation(defaultValidator.testDatabaseConnection);
53
+ mockValidator.checkPrismaClientGenerated.mockImplementation(defaultValidator.checkPrismaClientGenerated);
54
+
55
+ // Mock dotenv
56
+ dotenv.config = jest.fn();
57
+
28
58
  // Clear environment variables
29
59
  delete process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE;
30
60
  delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
@@ -40,32 +70,37 @@ describe('startCommand', () => {
40
70
  });
41
71
 
42
72
  afterEach(() => {
73
+ // Restore process.exit
74
+ if (mockProcessExit) {
75
+ mockProcessExit.mockRestore();
76
+ }
77
+
43
78
  // Clean up environment
44
79
  delete process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE;
45
80
  delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
46
81
  });
47
82
 
48
- it('should set FRIGG_SKIP_AWS_DISCOVERY to true in the parent process', () => {
83
+ it('should set FRIGG_SKIP_AWS_DISCOVERY to true in the parent process', async () => {
49
84
  const options = { stage: 'dev' };
50
85
 
51
- startCommand(options);
86
+ await startCommand(options);
52
87
 
53
88
  // Verify the environment variable is set in the parent process
54
89
  expect(process.env.FRIGG_SKIP_AWS_DISCOVERY).toBe('true');
55
90
  });
56
91
 
57
- it('should set AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE to suppress warnings', () => {
92
+ it('should set AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE to suppress warnings', async () => {
58
93
  const options = { stage: 'dev' };
59
94
 
60
- startCommand(options);
95
+ await startCommand(options);
61
96
 
62
97
  expect(process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE).toBe('1');
63
98
  });
64
99
 
65
- it('should spawn serverless with correct arguments', () => {
100
+ it('should spawn serverless with correct arguments', async () => {
66
101
  const options = { stage: 'prod' };
67
102
 
68
- startCommand(options);
103
+ await startCommand(options);
69
104
 
70
105
  expect(spawn).toHaveBeenCalledWith(
71
106
  'serverless',
@@ -80,10 +115,10 @@ describe('startCommand', () => {
80
115
  );
81
116
  });
82
117
 
83
- it('should include verbose flag when verbose option is enabled', () => {
118
+ it('should include verbose flag when verbose option is enabled', async () => {
84
119
  const options = { stage: 'dev', verbose: true };
85
120
 
86
- startCommand(options);
121
+ await startCommand(options);
87
122
 
88
123
  expect(spawn).toHaveBeenCalledWith(
89
124
  'serverless',
@@ -92,10 +127,10 @@ describe('startCommand', () => {
92
127
  );
93
128
  });
94
129
 
95
- it('should pass FRIGG_SKIP_AWS_DISCOVERY in spawn environment', () => {
130
+ it('should pass FRIGG_SKIP_AWS_DISCOVERY in spawn environment', async () => {
96
131
  const options = { stage: 'dev' };
97
132
 
98
- startCommand(options);
133
+ await startCommand(options);
99
134
 
100
135
  const spawnCall = spawn.mock.calls[0];
101
136
  const spawnOptions = spawnCall[2];
@@ -103,11 +138,11 @@ describe('startCommand', () => {
103
138
  expect(spawnOptions.env).toHaveProperty('FRIGG_SKIP_AWS_DISCOVERY', 'true');
104
139
  });
105
140
 
106
- it('should handle child process errors', () => {
141
+ it('should handle child process errors', async () => {
107
142
  const options = { stage: 'dev' };
108
143
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
109
144
 
110
- startCommand(options);
145
+ await startCommand(options);
111
146
 
112
147
  // Simulate an error
113
148
  const errorCallback = mockChildProcess.on.mock.calls.find(call => call[0] === 'error')[1];
@@ -119,11 +154,11 @@ describe('startCommand', () => {
119
154
  consoleErrorSpy.mockRestore();
120
155
  });
121
156
 
122
- it('should handle child process exit with non-zero code', () => {
157
+ it('should handle child process exit with non-zero code', async () => {
123
158
  const options = { stage: 'dev' };
124
159
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
125
160
 
126
- startCommand(options);
161
+ await startCommand(options);
127
162
 
128
163
  // Simulate exit with error code
129
164
  const closeCallback = mockChildProcess.on.mock.calls.find(call => call[0] === 'close')[1];
@@ -134,11 +169,11 @@ describe('startCommand', () => {
134
169
  consoleLogSpy.mockRestore();
135
170
  });
136
171
 
137
- it('should not log on successful exit', () => {
172
+ it('should not log on successful exit', async () => {
138
173
  const options = { stage: 'dev' };
139
174
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
140
175
 
141
- startCommand(options);
176
+ await startCommand(options);
142
177
 
143
178
  // Clear the spy calls from startCommand execution
144
179
  consoleLogSpy.mockClear();
@@ -152,4 +187,121 @@ describe('startCommand', () => {
152
187
 
153
188
  consoleLogSpy.mockRestore();
154
189
  });
190
+
191
+ describe('Database Pre-flight Validation', () => {
192
+ let mockConsoleError;
193
+
194
+ beforeEach(() => {
195
+ // Mock console.error (all other mocks are set up in outer beforeEach)
196
+ mockConsoleError = jest.spyOn(console, 'error').mockImplementation();
197
+ });
198
+
199
+ afterEach(() => {
200
+ mockConsoleError.mockRestore();
201
+ });
202
+
203
+ it('should pass pre-flight checks when database valid', async () => {
204
+ const options = { stage: 'dev' };
205
+
206
+ await startCommand(options);
207
+
208
+ expect(mockValidator.validateDatabaseUrl).toHaveBeenCalled();
209
+ expect(mockValidator.getDatabaseType).toHaveBeenCalled();
210
+ expect(mockValidator.testDatabaseConnection).toHaveBeenCalled();
211
+ expect(mockValidator.checkPrismaClientGenerated).toHaveBeenCalled();
212
+ expect(mockProcessExit).not.toHaveBeenCalled();
213
+ expect(spawn).toHaveBeenCalled();
214
+ });
215
+
216
+ it('should fail when DATABASE_URL missing', async () => {
217
+ mockValidator.validateDatabaseUrl.mockReturnValue({
218
+ valid: false,
219
+ error: 'DATABASE_URL not found'
220
+ });
221
+
222
+ await startCommand({});
223
+
224
+ expect(mockConsoleError).toHaveBeenCalled();
225
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
226
+ expect(spawn).not.toHaveBeenCalled();
227
+ });
228
+
229
+ it('should fail when database type not configured', async () => {
230
+ mockValidator.getDatabaseType.mockReturnValue({
231
+ error: 'Database not configured'
232
+ });
233
+
234
+ await startCommand({});
235
+
236
+ expect(mockConsoleError).toHaveBeenCalled();
237
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
238
+ expect(spawn).not.toHaveBeenCalled();
239
+ });
240
+
241
+ it('should fail when database connection fails', async () => {
242
+ mockValidator.testDatabaseConnection.mockResolvedValue({
243
+ connected: false,
244
+ error: 'Connection failed'
245
+ });
246
+
247
+ await startCommand({});
248
+
249
+ expect(mockConsoleError).toHaveBeenCalled();
250
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
251
+ expect(spawn).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it('should fail when Prisma client not generated', async () => {
255
+ mockValidator.checkPrismaClientGenerated.mockReturnValue({
256
+ generated: false,
257
+ error: 'Client not found'
258
+ });
259
+
260
+ await startCommand({});
261
+
262
+ expect(mockConsoleError).toHaveBeenCalled();
263
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('frigg db:setup'));
264
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
265
+ expect(spawn).not.toHaveBeenCalled();
266
+ });
267
+
268
+ it('should suggest running frigg db:setup when client missing', async () => {
269
+ mockValidator.checkPrismaClientGenerated.mockReturnValue({
270
+ generated: false,
271
+ error: 'Client not generated'
272
+ });
273
+
274
+ await startCommand({});
275
+
276
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('frigg db:setup'));
277
+ });
278
+
279
+ it('should exit with code 1 on validation failure', async () => {
280
+ mockValidator.validateDatabaseUrl.mockReturnValue({
281
+ valid: false
282
+ });
283
+
284
+ await startCommand({});
285
+
286
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
287
+ });
288
+
289
+ it('should continue to serverless start when validation passes', async () => {
290
+ await startCommand({ stage: 'dev' });
291
+
292
+ expect(spawn).toHaveBeenCalledWith(
293
+ 'serverless',
294
+ expect.arrayContaining(['offline']),
295
+ expect.any(Object)
296
+ );
297
+ });
298
+
299
+ it('should load .env before validation', async () => {
300
+ await startCommand({});
301
+
302
+ expect(dotenv.config).toHaveBeenCalledWith(expect.objectContaining({
303
+ path: expect.stringContaining('.env')
304
+ }));
305
+ });
306
+ });
155
307
  });