@friggframework/devtools 2.0.0-next.41 → 2.0.0-next.43

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 (34) hide show
  1. package/frigg-cli/__tests__/unit/commands/build.test.js +173 -405
  2. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +548 -0
  3. package/frigg-cli/__tests__/unit/commands/install.test.js +359 -377
  4. package/frigg-cli/__tests__/unit/commands/ui.test.js +266 -512
  5. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +366 -0
  6. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +304 -0
  7. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +486 -0
  8. package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
  9. package/frigg-cli/__tests__/utils/test-setup.js +22 -21
  10. package/frigg-cli/db-setup-command/index.js +186 -0
  11. package/frigg-cli/generate-command/__tests__/generate-command.test.js +151 -162
  12. package/frigg-cli/generate-iam-command.js +7 -4
  13. package/frigg-cli/index.js +9 -1
  14. package/frigg-cli/install-command/index.js +1 -1
  15. package/frigg-cli/jest.config.js +124 -0
  16. package/frigg-cli/package.json +4 -1
  17. package/frigg-cli/start-command/index.js +95 -2
  18. package/frigg-cli/start-command/start-command.test.js +161 -19
  19. package/frigg-cli/utils/database-validator.js +158 -0
  20. package/frigg-cli/utils/error-messages.js +257 -0
  21. package/frigg-cli/utils/prisma-runner.js +280 -0
  22. package/infrastructure/CLAUDE.md +481 -0
  23. package/infrastructure/IAM-POLICY-TEMPLATES.md +30 -12
  24. package/infrastructure/create-frigg-infrastructure.js +0 -2
  25. package/infrastructure/iam-generator.js +18 -38
  26. package/infrastructure/iam-generator.test.js +40 -8
  27. package/management-ui/src/App.jsx +1 -85
  28. package/management-ui/src/hooks/useFrigg.jsx +1 -215
  29. package/package.json +6 -6
  30. package/test/index.js +2 -4
  31. package/test/mock-integration.js +4 -14
  32. package/frigg-cli/__tests__/jest.config.js +0 -102
  33. package/frigg-cli/__tests__/utils/command-tester.js +0 -170
  34. package/test/auther-definition-tester.js +0 -125
@@ -55,12 +55,15 @@ async function generateIamCommand(options = {}) {
55
55
 
56
56
  // Generate the CloudFormation template
57
57
  console.log('\\nšŸ—ļø Generating IAM CloudFormation template...');
58
-
58
+
59
59
  const deploymentUserName = options.user || 'frigg-deployment-user';
60
60
  const stackName = options.stackName || 'frigg-deployment-iam';
61
-
62
- const cloudFormationYaml = generateIAMCloudFormation(appDefinition, {
63
- deploymentUserName,
61
+
62
+ // Use the summary already extracted above (line 44)
63
+ const cloudFormationYaml = generateIAMCloudFormation({
64
+ appName: summary.appName,
65
+ features: summary.features,
66
+ userPrefix: deploymentUserName,
64
67
  stackName
65
68
  });
66
69
 
@@ -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 };
@@ -9,7 +9,7 @@ const {
9
9
  validatePackageExists,
10
10
  searchAndSelectPackage,
11
11
  } = require('./validate-package');
12
- const { findNearestBackendPackageJson, validateBackendPath } = require('@friggframework/core');
12
+ const { findNearestBackendPackageJson, validateBackendPath } = require('@friggframework/core/utils');
13
13
 
14
14
  const installCommand = async (apiModuleName) => {
15
15
  try {
@@ -0,0 +1,124 @@
1
+ module.exports = {
2
+ displayName: 'Frigg CLI Tests',
3
+ testMatch: [
4
+ '<rootDir>/__tests__/**/*.test.js',
5
+ '<rootDir>/__tests__/**/*.spec.js',
6
+ '<rootDir>/**/start-command.test.js',
7
+ '<rootDir>/**/__tests__/**/*.test.js'
8
+ ],
9
+ // Exclude utility files and config from being treated as tests
10
+ testPathIgnorePatterns: [
11
+ '/node_modules/',
12
+ '/__tests__/utils/',
13
+ '/__tests__/jest.config.js',
14
+ '/test-setup.js'
15
+ ],
16
+ testEnvironment: 'node',
17
+ collectCoverageFrom: [
18
+ '**/*.js',
19
+ '!**/*.test.js',
20
+ '!**/*.spec.js',
21
+ '!**/node_modules/**',
22
+ '!**/__tests__/**',
23
+ '!**/coverage/**'
24
+ ],
25
+ coverageDirectory: 'coverage',
26
+ coverageReporters: [
27
+ 'text',
28
+ 'text-summary',
29
+ 'html',
30
+ 'lcov',
31
+ 'json'
32
+ ],
33
+ coverageThreshold: {
34
+ global: {
35
+ branches: 85,
36
+ functions: 85,
37
+ lines: 85,
38
+ statements: 85
39
+ },
40
+ './install-command/index.js': {
41
+ branches: 90,
42
+ functions: 90,
43
+ lines: 90,
44
+ statements: 90
45
+ },
46
+ './build-command/index.js': {
47
+ branches: 90,
48
+ functions: 90,
49
+ lines: 90,
50
+ statements: 90
51
+ },
52
+ './deploy-command/index.js': {
53
+ branches: 90,
54
+ functions: 90,
55
+ lines: 90,
56
+ statements: 90
57
+ },
58
+ './ui-command/index.js': {
59
+ branches: 90,
60
+ functions: 90,
61
+ lines: 90,
62
+ statements: 90
63
+ },
64
+ './generate-command/index.js': {
65
+ branches: 90,
66
+ functions: 90,
67
+ lines: 90,
68
+ statements: 90
69
+ },
70
+ './db-setup-command/index.js': {
71
+ branches: 90,
72
+ functions: 90,
73
+ lines: 90,
74
+ statements: 90
75
+ },
76
+ './utils/database-validator.js': {
77
+ branches: 85,
78
+ functions: 85,
79
+ lines: 85,
80
+ statements: 85
81
+ },
82
+ './utils/prisma-runner.js': {
83
+ branches: 85,
84
+ functions: 85,
85
+ lines: 85,
86
+ statements: 85
87
+ },
88
+ './utils/error-messages.js': {
89
+ branches: 85,
90
+ functions: 85,
91
+ lines: 85,
92
+ statements: 85
93
+ }
94
+ },
95
+ setupFilesAfterEnv: [
96
+ '<rootDir>/__tests__/utils/test-setup.js'
97
+ ],
98
+ testTimeout: 10000,
99
+ maxWorkers: '50%',
100
+ verbose: true,
101
+ collectCoverage: true,
102
+ coveragePathIgnorePatterns: [
103
+ '/node_modules/',
104
+ '/__tests__/',
105
+ '/coverage/',
106
+ '.test.js',
107
+ '.spec.js'
108
+ ],
109
+ moduleFileExtensions: [
110
+ 'js',
111
+ 'json',
112
+ 'node'
113
+ ],
114
+ transform: {},
115
+ // testResultsProcessor: 'jest-sonar-reporter', // Optional dependency
116
+ reporters: [
117
+ 'default'
118
+ // jest-junit reporter removed - optional dependency
119
+ ],
120
+ watchman: false,
121
+ forceExit: true,
122
+ detectOpenHandles: true,
123
+ errorOnDeprecated: true
124
+ };
@@ -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,14 +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
+ checkPrismaClientGenerated
9
+ } = require('../utils/database-validator');
10
+ const {
11
+ getDatabaseUrlMissingError,
12
+ getDatabaseTypeNotConfiguredError,
13
+ getPrismaClientNotGeneratedError
14
+ } = require('../utils/error-messages');
3
15
 
4
- function startCommand(options) {
16
+ async function startCommand(options) {
5
17
  if (options.verbose) {
6
18
  console.log('Verbose mode enabled');
7
19
  console.log('Options:', options);
8
20
  }
21
+
22
+ console.log(chalk.blue('šŸš€ Starting Frigg application...\n'));
23
+
24
+ // Load environment variables from .env file
25
+ const envPath = path.join(process.cwd(), '.env');
26
+ dotenv.config({ path: envPath });
27
+
28
+ // Pre-flight database checks
29
+ try {
30
+ await performDatabaseChecks(options.verbose);
31
+ } catch (error) {
32
+ console.error(chalk.red('\nāŒ Pre-flight checks failed'));
33
+ console.error(chalk.gray('Fix the issues above before starting the application.\n'));
34
+ process.exit(1);
35
+ }
36
+
37
+ console.log(chalk.green('āœ“ Database checks passed\n'));
9
38
  console.log('Starting backend and optional frontend...');
39
+
10
40
  // Suppress AWS SDK warning message about maintenance mode
11
- process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = 1;
41
+ process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = '1';
12
42
  // Skip AWS discovery for local development
13
43
  process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
14
44
  const backendPath = path.resolve(process.cwd());
@@ -53,4 +83,67 @@ function startCommand(options) {
53
83
  });
54
84
  }
55
85
 
86
+ /**
87
+ * Performs pre-flight database validation checks
88
+ * @param {boolean} verbose - Enable verbose output
89
+ * @throws {Error} If any validation check fails
90
+ */
91
+ async function performDatabaseChecks(verbose) {
92
+ // Check 1: Validate DATABASE_URL exists
93
+ if (verbose) {
94
+ console.log(chalk.gray('Checking DATABASE_URL...'));
95
+ }
96
+
97
+ const urlValidation = validateDatabaseUrl();
98
+ if (!urlValidation.valid) {
99
+ console.error(getDatabaseUrlMissingError());
100
+ throw new Error('DATABASE_URL validation failed');
101
+ }
102
+
103
+ if (verbose) {
104
+ console.log(chalk.green('āœ“ DATABASE_URL found'));
105
+ }
106
+
107
+ // Check 2: Determine database type
108
+ if (verbose) {
109
+ console.log(chalk.gray('Determining database type...'));
110
+ }
111
+
112
+ const dbTypeResult = getDatabaseType();
113
+ if (dbTypeResult.error) {
114
+ console.error(chalk.red('āŒ ' + dbTypeResult.error));
115
+ console.error(getDatabaseTypeNotConfiguredError());
116
+ throw new Error('Database type determination failed');
117
+ }
118
+
119
+ const dbType = dbTypeResult.dbType;
120
+
121
+ if (verbose) {
122
+ console.log(chalk.green(`āœ“ Database type: ${dbType}`));
123
+ }
124
+
125
+ // Check 3: Verify Prisma client is generated (BEFORE connection test to prevent auto-generation)
126
+ if (verbose) {
127
+ console.log(chalk.gray('Checking Prisma client...'));
128
+ }
129
+
130
+ const clientCheck = checkPrismaClientGenerated(dbType);
131
+
132
+ if (!clientCheck.generated) {
133
+ console.error(getPrismaClientNotGeneratedError(dbType));
134
+ console.error(chalk.yellow('\nRun this command to generate the Prisma client:'));
135
+ console.error(chalk.cyan(' frigg db:setup\n'));
136
+ throw new Error('Prisma client not generated');
137
+ }
138
+
139
+ if (verbose) {
140
+ console.log(chalk.green('āœ“ Prisma client generated'));
141
+ }
142
+
143
+ // Note: We skip connection testing in the start command because when using frigg:local,
144
+ // the CLI code runs from tmp/frigg but the client is in backend/node_modules,
145
+ // causing module resolution mismatches. The backend will test its own database
146
+ // connection when it starts.
147
+ }
148
+
56
149
  module.exports = { startCommand };
@@ -5,26 +5,60 @@
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
+ checkPrismaClientGenerated: jest.fn()
19
+ };
15
20
 
16
- // Mock the spawn function
17
21
  jest.mock('node:child_process', () => ({
18
22
  spawn: jest.fn(),
19
23
  }));
20
24
 
25
+ jest.mock('../utils/database-validator', () => mockValidator);
26
+ jest.mock('dotenv');
27
+
28
+ const { spawn } = require('node:child_process');
29
+ const { startCommand } = require('./index');
30
+ const { createMockDatabaseValidator } = require('../__tests__/utils/prisma-mock');
31
+ const dotenv = require('dotenv');
32
+
21
33
  describe('startCommand', () => {
22
34
  let mockChildProcess;
35
+ let mockProcessExit;
23
36
 
24
37
  beforeEach(() => {
38
+ // Mock process.exit to throw error and stop execution (prevents actual exits)
39
+ const exitError = new Error('process.exit called');
40
+ exitError.code = 'PROCESS_EXIT';
41
+ mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {
42
+ throw exitError;
43
+ });
44
+
25
45
  // Reset mocks
26
46
  jest.clearAllMocks();
27
47
 
48
+ // Re-apply process.exit mock after clearAllMocks
49
+ mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {
50
+ throw exitError;
51
+ });
52
+
53
+ // Set up default database validator mocks for all tests
54
+ const defaultValidator = createMockDatabaseValidator();
55
+ mockValidator.validateDatabaseUrl.mockReturnValue(defaultValidator.validateDatabaseUrl());
56
+ mockValidator.getDatabaseType.mockReturnValue(defaultValidator.getDatabaseType());
57
+ mockValidator.checkPrismaClientGenerated.mockReturnValue(defaultValidator.checkPrismaClientGenerated());
58
+
59
+ // Mock dotenv
60
+ dotenv.config = jest.fn();
61
+
28
62
  // Clear environment variables
29
63
  delete process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE;
30
64
  delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
@@ -40,32 +74,37 @@ describe('startCommand', () => {
40
74
  });
41
75
 
42
76
  afterEach(() => {
77
+ // Restore process.exit
78
+ if (mockProcessExit) {
79
+ mockProcessExit.mockRestore();
80
+ }
81
+
43
82
  // Clean up environment
44
83
  delete process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE;
45
84
  delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
46
85
  });
47
86
 
48
- it('should set FRIGG_SKIP_AWS_DISCOVERY to true in the parent process', () => {
87
+ it('should set FRIGG_SKIP_AWS_DISCOVERY to true in the parent process', async () => {
49
88
  const options = { stage: 'dev' };
50
89
 
51
- startCommand(options);
90
+ await startCommand(options);
52
91
 
53
92
  // Verify the environment variable is set in the parent process
54
93
  expect(process.env.FRIGG_SKIP_AWS_DISCOVERY).toBe('true');
55
94
  });
56
95
 
57
- it('should set AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE to suppress warnings', () => {
96
+ it('should set AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE to suppress warnings', async () => {
58
97
  const options = { stage: 'dev' };
59
98
 
60
- startCommand(options);
99
+ await startCommand(options);
61
100
 
62
101
  expect(process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE).toBe('1');
63
102
  });
64
103
 
65
- it('should spawn serverless with correct arguments', () => {
104
+ it('should spawn serverless with correct arguments', async () => {
66
105
  const options = { stage: 'prod' };
67
106
 
68
- startCommand(options);
107
+ await startCommand(options);
69
108
 
70
109
  expect(spawn).toHaveBeenCalledWith(
71
110
  'serverless',
@@ -80,10 +119,10 @@ describe('startCommand', () => {
80
119
  );
81
120
  });
82
121
 
83
- it('should include verbose flag when verbose option is enabled', () => {
122
+ it('should include verbose flag when verbose option is enabled', async () => {
84
123
  const options = { stage: 'dev', verbose: true };
85
124
 
86
- startCommand(options);
125
+ await startCommand(options);
87
126
 
88
127
  expect(spawn).toHaveBeenCalledWith(
89
128
  'serverless',
@@ -92,10 +131,10 @@ describe('startCommand', () => {
92
131
  );
93
132
  });
94
133
 
95
- it('should pass FRIGG_SKIP_AWS_DISCOVERY in spawn environment', () => {
134
+ it('should pass FRIGG_SKIP_AWS_DISCOVERY in spawn environment', async () => {
96
135
  const options = { stage: 'dev' };
97
136
 
98
- startCommand(options);
137
+ await startCommand(options);
99
138
 
100
139
  const spawnCall = spawn.mock.calls[0];
101
140
  const spawnOptions = spawnCall[2];
@@ -103,11 +142,11 @@ describe('startCommand', () => {
103
142
  expect(spawnOptions.env).toHaveProperty('FRIGG_SKIP_AWS_DISCOVERY', 'true');
104
143
  });
105
144
 
106
- it('should handle child process errors', () => {
145
+ it('should handle child process errors', async () => {
107
146
  const options = { stage: 'dev' };
108
147
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
109
148
 
110
- startCommand(options);
149
+ await startCommand(options);
111
150
 
112
151
  // Simulate an error
113
152
  const errorCallback = mockChildProcess.on.mock.calls.find(call => call[0] === 'error')[1];
@@ -119,11 +158,11 @@ describe('startCommand', () => {
119
158
  consoleErrorSpy.mockRestore();
120
159
  });
121
160
 
122
- it('should handle child process exit with non-zero code', () => {
161
+ it('should handle child process exit with non-zero code', async () => {
123
162
  const options = { stage: 'dev' };
124
163
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
125
164
 
126
- startCommand(options);
165
+ await startCommand(options);
127
166
 
128
167
  // Simulate exit with error code
129
168
  const closeCallback = mockChildProcess.on.mock.calls.find(call => call[0] === 'close')[1];
@@ -134,11 +173,11 @@ describe('startCommand', () => {
134
173
  consoleLogSpy.mockRestore();
135
174
  });
136
175
 
137
- it('should not log on successful exit', () => {
176
+ it('should not log on successful exit', async () => {
138
177
  const options = { stage: 'dev' };
139
178
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
140
179
 
141
- startCommand(options);
180
+ await startCommand(options);
142
181
 
143
182
  // Clear the spy calls from startCommand execution
144
183
  consoleLogSpy.mockClear();
@@ -152,4 +191,107 @@ describe('startCommand', () => {
152
191
 
153
192
  consoleLogSpy.mockRestore();
154
193
  });
194
+
195
+ describe('Database Pre-flight Validation', () => {
196
+ let mockConsoleError;
197
+
198
+ beforeEach(() => {
199
+ // Mock console.error (all other mocks are set up in outer beforeEach)
200
+ mockConsoleError = jest.spyOn(console, 'error').mockImplementation();
201
+ });
202
+
203
+ afterEach(() => {
204
+ mockConsoleError.mockRestore();
205
+ });
206
+
207
+ it('should pass pre-flight checks when database valid', async () => {
208
+ const options = { stage: 'dev' };
209
+
210
+ await startCommand(options);
211
+
212
+ expect(mockValidator.validateDatabaseUrl).toHaveBeenCalled();
213
+ expect(mockValidator.getDatabaseType).toHaveBeenCalled();
214
+ expect(mockValidator.checkPrismaClientGenerated).toHaveBeenCalled();
215
+ expect(mockProcessExit).not.toHaveBeenCalled();
216
+ expect(spawn).toHaveBeenCalled();
217
+ });
218
+
219
+ it('should fail when DATABASE_URL missing', async () => {
220
+ mockValidator.validateDatabaseUrl.mockReturnValue({
221
+ valid: false,
222
+ error: 'DATABASE_URL not found'
223
+ });
224
+
225
+ await expect(startCommand({})).rejects.toThrow('process.exit called');
226
+
227
+ expect(mockConsoleError).toHaveBeenCalled();
228
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
229
+ expect(spawn).not.toHaveBeenCalled();
230
+ });
231
+
232
+ it('should fail when database type not configured', async () => {
233
+ mockValidator.getDatabaseType.mockReturnValue({
234
+ error: 'Database not configured'
235
+ });
236
+
237
+ await expect(startCommand({})).rejects.toThrow('process.exit called');
238
+
239
+ expect(mockConsoleError).toHaveBeenCalled();
240
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
241
+ expect(spawn).not.toHaveBeenCalled();
242
+ });
243
+
244
+ it('should fail when Prisma client not generated', async () => {
245
+ mockValidator.checkPrismaClientGenerated.mockReturnValue({
246
+ generated: false,
247
+ error: 'Client not found'
248
+ });
249
+
250
+ await expect(startCommand({})).rejects.toThrow('process.exit called');
251
+
252
+ expect(mockConsoleError).toHaveBeenCalled();
253
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('frigg db:setup'));
254
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
255
+ expect(spawn).not.toHaveBeenCalled();
256
+ });
257
+
258
+ it('should suggest running frigg db:setup when client missing', async () => {
259
+ mockValidator.checkPrismaClientGenerated.mockReturnValue({
260
+ generated: false,
261
+ error: 'Client not generated'
262
+ });
263
+
264
+ await expect(startCommand({})).rejects.toThrow('process.exit called');
265
+
266
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('frigg db:setup'));
267
+ });
268
+
269
+ it('should exit with code 1 on validation failure', async () => {
270
+ mockValidator.validateDatabaseUrl.mockReturnValue({
271
+ valid: false
272
+ });
273
+
274
+ await expect(startCommand({})).rejects.toThrow('process.exit called');
275
+
276
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
277
+ });
278
+
279
+ it('should continue to serverless start when validation passes', async () => {
280
+ await startCommand({ stage: 'dev' });
281
+
282
+ expect(spawn).toHaveBeenCalledWith(
283
+ 'serverless',
284
+ expect.arrayContaining(['offline']),
285
+ expect.any(Object)
286
+ );
287
+ });
288
+
289
+ it('should load .env before validation', async () => {
290
+ await startCommand({});
291
+
292
+ expect(dotenv.config).toHaveBeenCalledWith(expect.objectContaining({
293
+ path: expect.stringContaining('.env')
294
+ }));
295
+ });
296
+ });
155
297
  });