@geekmidas/cli 1.5.1 → 1.6.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 (71) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/{config-BYn5yUt5.cjs → config-6JHOwLCx.cjs} +30 -2
  3. package/dist/{config-dLNQIvDR.mjs.map → config-6JHOwLCx.cjs.map} +1 -1
  4. package/dist/{config-dLNQIvDR.mjs → config-DxASSNjr.mjs} +25 -3
  5. package/dist/{config-BYn5yUt5.cjs.map → config-DxASSNjr.mjs.map} +1 -1
  6. package/dist/config.cjs +3 -2
  7. package/dist/config.d.cts +14 -2
  8. package/dist/config.d.cts.map +1 -1
  9. package/dist/config.d.mts +14 -2
  10. package/dist/config.d.mts.map +1 -1
  11. package/dist/config.mjs +3 -3
  12. package/dist/{index-Bj5VNxEL.d.mts → index-C-KxSGGK.d.mts} +2 -2
  13. package/dist/{index-Ba21_lNt.d.cts.map → index-C-KxSGGK.d.mts.map} +1 -1
  14. package/dist/{index-Ba21_lNt.d.cts → index-Cyk2rTyj.d.cts} +2 -2
  15. package/dist/{index-Bj5VNxEL.d.mts.map → index-Cyk2rTyj.d.cts.map} +1 -1
  16. package/dist/index.cjs +549 -133
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +513 -97
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/{openapi-CMTyaIJJ.mjs → openapi-BYlyAbH3.mjs} +6 -5
  21. package/dist/openapi-BYlyAbH3.mjs.map +1 -0
  22. package/dist/{openapi-CqblwJZ4.cjs → openapi-CnvwSRDU.cjs} +6 -5
  23. package/dist/openapi-CnvwSRDU.cjs.map +1 -0
  24. package/dist/openapi.cjs +3 -3
  25. package/dist/openapi.d.cts +1 -0
  26. package/dist/openapi.d.cts.map +1 -1
  27. package/dist/openapi.d.mts +1 -0
  28. package/dist/openapi.d.mts.map +1 -1
  29. package/dist/openapi.mjs +3 -3
  30. package/dist/workspace/index.cjs +1 -1
  31. package/dist/workspace/index.d.cts +1 -1
  32. package/dist/workspace/index.d.mts +1 -1
  33. package/dist/workspace/index.mjs +1 -1
  34. package/dist/{workspace-Dy8k7Wru.mjs → workspace-9IQIjwkQ.mjs} +5 -3
  35. package/dist/workspace-9IQIjwkQ.mjs.map +1 -0
  36. package/dist/{workspace-DIMnYaYt.cjs → workspace-D2ocAlpl.cjs} +5 -3
  37. package/dist/workspace-D2ocAlpl.cjs.map +1 -0
  38. package/package.json +6 -5
  39. package/src/config.ts +44 -0
  40. package/src/dev/__tests__/index.spec.ts +490 -0
  41. package/src/dev/index.ts +313 -18
  42. package/src/generators/Generator.ts +4 -1
  43. package/src/init/__tests__/generators.spec.ts +167 -18
  44. package/src/init/__tests__/init.spec.ts +66 -3
  45. package/src/init/generators/auth.ts +6 -5
  46. package/src/init/generators/config.ts +49 -7
  47. package/src/init/generators/docker.ts +8 -8
  48. package/src/init/generators/index.ts +1 -0
  49. package/src/init/generators/models.ts +3 -5
  50. package/src/init/generators/package.ts +4 -0
  51. package/src/init/generators/test.ts +133 -0
  52. package/src/init/generators/ui.ts +13 -12
  53. package/src/init/generators/web.ts +9 -8
  54. package/src/init/index.ts +2 -0
  55. package/src/init/templates/api.ts +6 -6
  56. package/src/init/templates/minimal.ts +2 -2
  57. package/src/init/templates/worker.ts +2 -2
  58. package/src/init/versions.ts +3 -3
  59. package/src/openapi.ts +6 -2
  60. package/src/test/__tests__/__fixtures__/workspace.ts +104 -0
  61. package/src/test/__tests__/api.spec.ts +199 -0
  62. package/src/test/__tests__/auth.spec.ts +162 -0
  63. package/src/test/__tests__/index.spec.ts +323 -0
  64. package/src/test/__tests__/web.spec.ts +210 -0
  65. package/src/test/index.ts +165 -14
  66. package/src/workspace/__tests__/index.spec.ts +3 -0
  67. package/src/workspace/index.ts +4 -2
  68. package/dist/openapi-CMTyaIJJ.mjs.map +0 -1
  69. package/dist/openapi-CqblwJZ4.cjs.map +0 -1
  70. package/dist/workspace-DIMnYaYt.cjs.map +0 -1
  71. package/dist/workspace-Dy8k7Wru.mjs.map +0 -1
@@ -126,7 +126,7 @@ export type AppEvents =
126
126
  path: 'src/events/publisher.ts',
127
127
  content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
128
128
  import { Publisher, type EventPublisher } from '@geekmidas/events';
129
- import type { AppEvents } from './types.js';
129
+ import type { AppEvents } from './types.ts';
130
130
 
131
131
  export const eventsPublisherService = {
132
132
  serviceName: 'events' as const,
@@ -155,7 +155,7 @@ export const eventsPublisherService = {
155
155
  {
156
156
  path: 'src/subscribers/user-events.ts',
157
157
  content: `import { s } from '@geekmidas/constructs/subscribers';
158
- import { eventsPublisherService } from '../events/publisher.js';
158
+ import { eventsPublisherService } from '~/events/publisher.ts';
159
159
 
160
160
  export const userEventsSubscriber = s
161
161
  .publisher(eventsPublisherService)
@@ -32,16 +32,16 @@ export const GEEKMIDAS_VERSIONS = {
32
32
  '@geekmidas/cache': '~1.0.0',
33
33
  '@geekmidas/client': '~1.0.0',
34
34
  '@geekmidas/cloud': '~1.0.0',
35
- '@geekmidas/constructs': '~1.0.4',
35
+ '@geekmidas/constructs': '~1.0.5',
36
36
  '@geekmidas/db': '~1.0.0',
37
37
  '@geekmidas/emailkit': '~1.0.0',
38
- '@geekmidas/envkit': '~1.0.1',
38
+ '@geekmidas/envkit': '~1.0.2',
39
39
  '@geekmidas/errors': '~1.0.0',
40
40
  '@geekmidas/events': '~1.0.0',
41
41
  '@geekmidas/logger': '~1.0.0',
42
42
  '@geekmidas/rate-limit': '~1.0.0',
43
43
  '@geekmidas/schema': '~1.0.0',
44
- '@geekmidas/services': '~1.0.0',
44
+ '@geekmidas/services': '~1.0.1',
45
45
  '@geekmidas/storage': '~1.0.0',
46
46
  '@geekmidas/studio': '~1.0.0',
47
47
  '@geekmidas/telescope': '~1.0.0',
package/src/openapi.ts CHANGED
@@ -52,7 +52,7 @@ export function resolveOpenApiConfig(
52
52
  */
53
53
  export async function generateOpenApi(
54
54
  config: GkmConfig,
55
- options: { silent?: boolean } = {},
55
+ options: { silent?: boolean; bustCache?: boolean } = {},
56
56
  ): Promise<{ outputPath: string; endpointCount: number } | null> {
57
57
  const logger = options.silent ? { log: () => {} } : console;
58
58
  const openApiConfig = resolveOpenApiConfig(config);
@@ -62,7 +62,11 @@ export async function generateOpenApi(
62
62
  }
63
63
 
64
64
  const endpointGenerator = new EndpointGenerator();
65
- const loadedEndpoints = await endpointGenerator.load(config.routes);
65
+ const loadedEndpoints = await endpointGenerator.load(
66
+ config.routes,
67
+ undefined,
68
+ options.bustCache,
69
+ );
66
70
 
67
71
  if (loadedEndpoints.length === 0) {
68
72
  logger.log('No valid endpoints found for OpenAPI generation');
@@ -0,0 +1,104 @@
1
+ import { normalizeWorkspace } from '../../../workspace/index';
2
+
3
+ /**
4
+ * Simulates the secrets that gkm init generates for a fullstack workspace.
5
+ * In production these come from the encrypted store via toEmbeddableSecrets.
6
+ */
7
+ export function createFullstackSecrets(): Record<string, string> {
8
+ return {
9
+ NODE_ENV: 'development',
10
+ PORT: '3000',
11
+ LOG_LEVEL: 'debug',
12
+ JWT_SECRET: 'dev-jwt-secret',
13
+ // Per-app database URLs (fullstack workspace)
14
+ API_DATABASE_URL: 'postgresql://api:api-pass@localhost:5432/my-saas',
15
+ AUTH_DATABASE_URL: 'postgresql://auth:auth-pass@localhost:5432/my-saas',
16
+ API_DB_PASSWORD: 'api-pass',
17
+ AUTH_DB_PASSWORD: 'auth-pass',
18
+ // Auth service secrets
19
+ AUTH_PORT: '3002',
20
+ AUTH_URL: 'http://localhost:3002',
21
+ BETTER_AUTH_SECRET: 'better-auth-secret-123',
22
+ BETTER_AUTH_URL: 'http://localhost:3002',
23
+ BETTER_AUTH_TRUSTED_ORIGINS: 'http://localhost:3000,http://localhost:3001',
24
+ // Service credentials
25
+ POSTGRES_USER: 'api',
26
+ POSTGRES_PASSWORD: 'api-pass',
27
+ POSTGRES_DB: 'my-saas',
28
+ POSTGRES_HOST: 'localhost',
29
+ POSTGRES_PORT: '5432',
30
+ REDIS_PASSWORD: 'redis-pass',
31
+ REDIS_HOST: 'localhost',
32
+ REDIS_PORT: '6379',
33
+ // URLs
34
+ DATABASE_URL: 'postgresql://api:api-pass@localhost:5432/my-saas',
35
+ REDIS_URL: 'redis://:redis-pass@localhost:6379',
36
+ };
37
+ }
38
+
39
+ export function createFullstackWorkspace(): ReturnType<
40
+ typeof normalizeWorkspace
41
+ > {
42
+ return normalizeWorkspace(
43
+ {
44
+ apps: {
45
+ api: {
46
+ type: 'backend',
47
+ path: 'apps/api',
48
+ port: 3000,
49
+ routes: './src/endpoints/**/*.ts',
50
+ dependencies: [],
51
+ },
52
+ auth: {
53
+ type: 'backend',
54
+ path: 'apps/auth',
55
+ port: 3002,
56
+ entry: './src/index.ts',
57
+ dependencies: [],
58
+ },
59
+ web: {
60
+ type: 'frontend',
61
+ path: 'apps/web',
62
+ port: 3001,
63
+ framework: 'nextjs',
64
+ dependencies: ['api', 'auth'],
65
+ },
66
+ },
67
+ },
68
+ '/project',
69
+ );
70
+ }
71
+
72
+ /**
73
+ * Simulates what loadSecretsForApp does for a specific app:
74
+ * maps {APP}_DATABASE_URL -> DATABASE_URL.
75
+ */
76
+ export function mapSecretsForApp(
77
+ secrets: Record<string, string>,
78
+ appName: string,
79
+ ): Record<string, string> {
80
+ const prefix = appName.toUpperCase();
81
+ const mapped = { ...secrets };
82
+ const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
83
+ if (appDbUrl) {
84
+ mapped.DATABASE_URL = appDbUrl;
85
+ }
86
+ return mapped;
87
+ }
88
+
89
+ export const COMPOSE_FULL = `
90
+ services:
91
+ postgres:
92
+ image: postgres:17
93
+ ports:
94
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
95
+ redis:
96
+ image: redis:7
97
+ ports:
98
+ - '\${REDIS_HOST_PORT:-6379}:6379'
99
+ mailpit:
100
+ image: axllent/mailpit
101
+ ports:
102
+ - '\${MAILPIT_SMTP_PORT:-1025}:1025'
103
+ - '\${MAILPIT_UI_PORT:-8025}:8025'
104
+ `;
@@ -0,0 +1,199 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import {
5
+ afterAll,
6
+ afterEach,
7
+ beforeAll,
8
+ beforeEach,
9
+ describe,
10
+ expect,
11
+ it,
12
+ vi,
13
+ } from 'vitest';
14
+ import {
15
+ loadPortState,
16
+ parseComposePortMappings,
17
+ rewriteUrlsWithPorts,
18
+ savePortState,
19
+ } from '../../dev/index';
20
+ import { getDependencyEnvVars } from '../../workspace/index';
21
+ import { rewriteDatabaseUrlForTests } from '../index';
22
+ import {
23
+ COMPOSE_FULL,
24
+ createFullstackSecrets,
25
+ createFullstackWorkspace,
26
+ mapSecretsForApp,
27
+ } from './__fixtures__/workspace';
28
+
29
+ beforeAll(() => {
30
+ vi.spyOn(console, 'log').mockImplementation(() => {});
31
+ });
32
+
33
+ afterAll(() => {
34
+ vi.restoreAllMocks();
35
+ });
36
+
37
+ describe('api app context', () => {
38
+ let testDir: string;
39
+
40
+ beforeEach(() => {
41
+ testDir = join(tmpdir(), `gkm-api-test-${Date.now()}`);
42
+ mkdirSync(testDir, { recursive: true });
43
+ writeFileSync(join(testDir, 'docker-compose.yml'), COMPOSE_FULL);
44
+ });
45
+
46
+ afterEach(() => {
47
+ rmSync(testDir, { recursive: true, force: true });
48
+ });
49
+
50
+ it('should resolve DATABASE_URL from API_DATABASE_URL', () => {
51
+ const apiSecrets = mapSecretsForApp(createFullstackSecrets(), 'api');
52
+
53
+ expect(apiSecrets.DATABASE_URL).toBe(
54
+ 'postgresql://api:api-pass@localhost:5432/my-saas',
55
+ );
56
+ expect(apiSecrets.API_DATABASE_URL).toBe(
57
+ 'postgresql://api:api-pass@localhost:5432/my-saas',
58
+ );
59
+ });
60
+
61
+ it('should rewrite DATABASE_URL and REDIS_URL when ports are remapped', async () => {
62
+ await savePortState(testDir, {
63
+ POSTGRES_HOST_PORT: 5433,
64
+ REDIS_HOST_PORT: 6380,
65
+ });
66
+
67
+ let secrets = mapSecretsForApp(createFullstackSecrets(), 'api');
68
+
69
+ const mappings = parseComposePortMappings(
70
+ join(testDir, 'docker-compose.yml'),
71
+ );
72
+ const ports = await loadPortState(testDir);
73
+ secrets = rewriteUrlsWithPorts(secrets, {
74
+ dockerEnv: {},
75
+ ports,
76
+ mappings,
77
+ });
78
+
79
+ expect(secrets.DATABASE_URL).toBe(
80
+ 'postgresql://api:api-pass@localhost:5433/my-saas',
81
+ );
82
+ expect(secrets.API_DATABASE_URL).toBe(
83
+ 'postgresql://api:api-pass@localhost:5433/my-saas',
84
+ );
85
+ expect(secrets.REDIS_URL).toBe('redis://:redis-pass@localhost:6380');
86
+ // AUTH_URL is an app URL, not a docker service
87
+ expect(secrets.AUTH_URL).toBe('http://localhost:3002');
88
+ });
89
+
90
+ it('should rewrite only postgres when redis port unchanged', async () => {
91
+ await savePortState(testDir, {
92
+ POSTGRES_HOST_PORT: 5433,
93
+ REDIS_HOST_PORT: 6379,
94
+ });
95
+
96
+ let secrets = mapSecretsForApp(createFullstackSecrets(), 'api');
97
+
98
+ const mappings = parseComposePortMappings(
99
+ join(testDir, 'docker-compose.yml'),
100
+ );
101
+ const ports = await loadPortState(testDir);
102
+ secrets = rewriteUrlsWithPorts(secrets, {
103
+ dockerEnv: {},
104
+ ports,
105
+ mappings,
106
+ });
107
+
108
+ expect(secrets.DATABASE_URL).toBe(
109
+ 'postgresql://api:api-pass@localhost:5433/my-saas',
110
+ );
111
+ // Redis port unchanged — URL stays the same
112
+ expect(secrets.REDIS_URL).toBe('redis://:redis-pass@localhost:6379');
113
+ });
114
+
115
+ it('should apply full pipeline for gkm test in api context', async () => {
116
+ await savePortState(testDir, {
117
+ POSTGRES_HOST_PORT: 5433,
118
+ REDIS_HOST_PORT: 6380,
119
+ });
120
+
121
+ let secrets = mapSecretsForApp(createFullstackSecrets(), 'api');
122
+
123
+ const mappings = parseComposePortMappings(
124
+ join(testDir, 'docker-compose.yml'),
125
+ );
126
+ const ports = await loadPortState(testDir);
127
+ secrets = rewriteUrlsWithPorts(secrets, {
128
+ dockerEnv: {},
129
+ ports,
130
+ mappings,
131
+ });
132
+ secrets = rewriteDatabaseUrlForTests(secrets);
133
+
134
+ // Port rewrite + _test suffix
135
+ expect(secrets.DATABASE_URL).toBe(
136
+ 'postgresql://api:api-pass@localhost:5433/my-saas_test',
137
+ );
138
+ expect(secrets.API_DATABASE_URL).toBe(
139
+ 'postgresql://api:api-pass@localhost:5433/my-saas_test',
140
+ );
141
+ // AUTH_DATABASE_URL also gets both rewrites
142
+ expect(secrets.AUTH_DATABASE_URL).toBe(
143
+ 'postgresql://auth:auth-pass@localhost:5433/my-saas_test',
144
+ );
145
+ // Non-database URLs unaffected
146
+ expect(secrets.REDIS_URL).toBe('redis://:redis-pass@localhost:6380');
147
+ expect(secrets.AUTH_URL).toBe('http://localhost:3002');
148
+ });
149
+
150
+ it('should use default ports when no port state saved', async () => {
151
+ let secrets = mapSecretsForApp(createFullstackSecrets(), 'api');
152
+
153
+ const mappings = parseComposePortMappings(
154
+ join(testDir, 'docker-compose.yml'),
155
+ );
156
+ const ports = await loadPortState(testDir);
157
+
158
+ if (Object.keys(ports).length > 0) {
159
+ secrets = rewriteUrlsWithPorts(secrets, {
160
+ dockerEnv: {},
161
+ ports,
162
+ mappings,
163
+ });
164
+ }
165
+ secrets = rewriteDatabaseUrlForTests(secrets);
166
+
167
+ expect(secrets.DATABASE_URL).toBe(
168
+ 'postgresql://api:api-pass@localhost:5432/my-saas_test',
169
+ );
170
+ expect(secrets.REDIS_URL).toBe('redis://:redis-pass@localhost:6379');
171
+ });
172
+
173
+ it('should not inject dependency URLs for api app (no dependencies)', () => {
174
+ const workspace = createFullstackWorkspace();
175
+ const depEnv = getDependencyEnvVars(workspace, 'api');
176
+
177
+ expect(depEnv).toEqual({});
178
+ });
179
+
180
+ it('should keep BETTER_AUTH vars available for api to call auth service', async () => {
181
+ await savePortState(testDir, { POSTGRES_HOST_PORT: 5433 });
182
+
183
+ let secrets = mapSecretsForApp(createFullstackSecrets(), 'api');
184
+
185
+ const mappings = parseComposePortMappings(
186
+ join(testDir, 'docker-compose.yml'),
187
+ );
188
+ const ports = await loadPortState(testDir);
189
+ secrets = rewriteUrlsWithPorts(secrets, {
190
+ dockerEnv: {},
191
+ ports,
192
+ mappings,
193
+ });
194
+
195
+ // API app may need AUTH_URL to call auth service internally
196
+ expect(secrets.AUTH_URL).toBe('http://localhost:3002');
197
+ expect(secrets.BETTER_AUTH_URL).toBe('http://localhost:3002');
198
+ });
199
+ });
@@ -0,0 +1,162 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import {
5
+ afterAll,
6
+ afterEach,
7
+ beforeAll,
8
+ beforeEach,
9
+ describe,
10
+ expect,
11
+ it,
12
+ vi,
13
+ } from 'vitest';
14
+ import {
15
+ loadPortState,
16
+ parseComposePortMappings,
17
+ rewriteUrlsWithPorts,
18
+ savePortState,
19
+ } from '../../dev/index';
20
+ import { getDependencyEnvVars } from '../../workspace/index';
21
+ import { rewriteDatabaseUrlForTests } from '../index';
22
+ import {
23
+ COMPOSE_FULL,
24
+ createFullstackSecrets,
25
+ createFullstackWorkspace,
26
+ mapSecretsForApp,
27
+ } from './__fixtures__/workspace';
28
+
29
+ beforeAll(() => {
30
+ vi.spyOn(console, 'log').mockImplementation(() => {});
31
+ });
32
+
33
+ afterAll(() => {
34
+ vi.restoreAllMocks();
35
+ });
36
+
37
+ describe('auth app context', () => {
38
+ let testDir: string;
39
+
40
+ beforeEach(() => {
41
+ testDir = join(tmpdir(), `gkm-auth-test-${Date.now()}`);
42
+ mkdirSync(testDir, { recursive: true });
43
+ writeFileSync(join(testDir, 'docker-compose.yml'), COMPOSE_FULL);
44
+ });
45
+
46
+ afterEach(() => {
47
+ rmSync(testDir, { recursive: true, force: true });
48
+ });
49
+
50
+ it('should resolve DATABASE_URL from AUTH_DATABASE_URL', () => {
51
+ const authSecrets = mapSecretsForApp(createFullstackSecrets(), 'auth');
52
+
53
+ expect(authSecrets.DATABASE_URL).toBe(
54
+ 'postgresql://auth:auth-pass@localhost:5432/my-saas',
55
+ );
56
+ expect(authSecrets.AUTH_DATABASE_URL).toBe(
57
+ 'postgresql://auth:auth-pass@localhost:5432/my-saas',
58
+ );
59
+ });
60
+
61
+ it('should preserve BETTER_AUTH_* secrets untouched', () => {
62
+ const authSecrets = mapSecretsForApp(createFullstackSecrets(), 'auth');
63
+
64
+ expect(authSecrets.BETTER_AUTH_SECRET).toBe('better-auth-secret-123');
65
+ expect(authSecrets.BETTER_AUTH_URL).toBe('http://localhost:3002');
66
+ expect(authSecrets.BETTER_AUTH_TRUSTED_ORIGINS).toBe(
67
+ 'http://localhost:3000,http://localhost:3001',
68
+ );
69
+ });
70
+
71
+ it('should rewrite auth DATABASE_URL ports when postgres is remapped', async () => {
72
+ await savePortState(testDir, {
73
+ POSTGRES_HOST_PORT: 5433,
74
+ REDIS_HOST_PORT: 6379,
75
+ });
76
+
77
+ let secrets = mapSecretsForApp(createFullstackSecrets(), 'auth');
78
+
79
+ const mappings = parseComposePortMappings(
80
+ join(testDir, 'docker-compose.yml'),
81
+ );
82
+ const ports = await loadPortState(testDir);
83
+ secrets = rewriteUrlsWithPorts(secrets, {
84
+ dockerEnv: {},
85
+ ports,
86
+ mappings,
87
+ });
88
+
89
+ expect(secrets.DATABASE_URL).toBe(
90
+ 'postgresql://auth:auth-pass@localhost:5433/my-saas',
91
+ );
92
+ expect(secrets.AUTH_DATABASE_URL).toBe(
93
+ 'postgresql://auth:auth-pass@localhost:5433/my-saas',
94
+ );
95
+ // BETTER_AUTH_URL is an app port (3002), not a docker service port
96
+ expect(secrets.BETTER_AUTH_URL).toBe('http://localhost:3002');
97
+ expect(secrets.BETTER_AUTH_SECRET).toBe('better-auth-secret-123');
98
+ expect(secrets.BETTER_AUTH_TRUSTED_ORIGINS).toBe(
99
+ 'http://localhost:3000,http://localhost:3001',
100
+ );
101
+ });
102
+
103
+ it('should apply _test suffix for gkm test in auth context', async () => {
104
+ await savePortState(testDir, { POSTGRES_HOST_PORT: 5433 });
105
+
106
+ let secrets = mapSecretsForApp(createFullstackSecrets(), 'auth');
107
+
108
+ const mappings = parseComposePortMappings(
109
+ join(testDir, 'docker-compose.yml'),
110
+ );
111
+ const ports = await loadPortState(testDir);
112
+ secrets = rewriteUrlsWithPorts(secrets, {
113
+ dockerEnv: {},
114
+ ports,
115
+ mappings,
116
+ });
117
+ secrets = rewriteDatabaseUrlForTests(secrets);
118
+
119
+ // Port rewrite + _test suffix
120
+ expect(secrets.DATABASE_URL).toBe(
121
+ 'postgresql://auth:auth-pass@localhost:5433/my-saas_test',
122
+ );
123
+ expect(secrets.AUTH_DATABASE_URL).toBe(
124
+ 'postgresql://auth:auth-pass@localhost:5433/my-saas_test',
125
+ );
126
+ // API_DATABASE_URL also gets _test (same container, different user)
127
+ expect(secrets.API_DATABASE_URL).toBe(
128
+ 'postgresql://api:api-pass@localhost:5433/my-saas_test',
129
+ );
130
+ });
131
+
132
+ it('should use default ports when no port state saved', async () => {
133
+ let secrets = mapSecretsForApp(createFullstackSecrets(), 'auth');
134
+
135
+ const mappings = parseComposePortMappings(
136
+ join(testDir, 'docker-compose.yml'),
137
+ );
138
+ const ports = await loadPortState(testDir);
139
+
140
+ // No saved state — rewriting is a no-op
141
+ if (Object.keys(ports).length > 0) {
142
+ secrets = rewriteUrlsWithPorts(secrets, {
143
+ dockerEnv: {},
144
+ ports,
145
+ mappings,
146
+ });
147
+ }
148
+ secrets = rewriteDatabaseUrlForTests(secrets);
149
+
150
+ // Default port preserved, only _test suffix added
151
+ expect(secrets.DATABASE_URL).toBe(
152
+ 'postgresql://auth:auth-pass@localhost:5432/my-saas_test',
153
+ );
154
+ });
155
+
156
+ it('should not inject dependency URLs for auth app (no dependencies)', () => {
157
+ const workspace = createFullstackWorkspace();
158
+ const depEnv = getDependencyEnvVars(workspace, 'auth');
159
+
160
+ expect(depEnv).toEqual({});
161
+ });
162
+ });