@open-mercato/cli 0.4.7 → 0.4.8-canary-d3f23076fd
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/lib/generators/module-registry.js +134 -0
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/testing/integration.js +109 -78
- package/dist/lib/testing/integration.js.map +2 -2
- package/package.json +6 -5
- package/src/lib/generators/__tests__/module-subset.test.ts +57 -1
- package/src/lib/generators/__tests__/scanner.test.ts +2 -1
- package/src/lib/generators/module-registry.ts +121 -0
- package/src/lib/testing/__tests__/integration.test.ts +106 -3
- package/src/lib/testing/integration.ts +137 -85
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8-canary-d3f23076fd",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -58,21 +58,22 @@
|
|
|
58
58
|
"@mikro-orm/core": "^6.6.2",
|
|
59
59
|
"@mikro-orm/migrations": "^6.6.2",
|
|
60
60
|
"@mikro-orm/postgresql": "^6.6.2",
|
|
61
|
-
"@open-mercato/shared": "0.4.
|
|
61
|
+
"@open-mercato/shared": "0.4.8-canary-d3f23076fd",
|
|
62
62
|
"pg": "8.20.0",
|
|
63
63
|
"testcontainers": "^11.12.0",
|
|
64
64
|
"typescript": "^5.9.3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@open-mercato/shared": "0.4.
|
|
67
|
+
"@open-mercato/shared": "0.4.8-canary-d3f23076fd"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@open-mercato/shared": "0.4.
|
|
70
|
+
"@open-mercato/shared": "0.4.8-canary-d3f23076fd",
|
|
71
71
|
"@types/jest": "^30.0.0",
|
|
72
72
|
"jest": "^30.2.0",
|
|
73
73
|
"ts-jest": "^29.4.6"
|
|
74
74
|
},
|
|
75
75
|
"publishConfig": {
|
|
76
76
|
"access": "public"
|
|
77
|
-
}
|
|
77
|
+
},
|
|
78
|
+
"stableVersion": "0.4.7"
|
|
78
79
|
}
|
|
@@ -361,7 +361,7 @@ describe('generateModuleRegistryCli with module subsets', () => {
|
|
|
361
361
|
})
|
|
362
362
|
|
|
363
363
|
describe('all generated files are valid with varying subsets', () => {
|
|
364
|
-
it('produces all
|
|
364
|
+
it('produces all generated files even when no modules have matching content', async () => {
|
|
365
365
|
scaffoldModule(tmpDir, 'bare_mod', 'pkg', ['acl.ts'])
|
|
366
366
|
const enabled: ModuleEntry[] = [
|
|
367
367
|
{ id: 'bare_mod', from: '@open-mercato/core' },
|
|
@@ -380,6 +380,10 @@ describe('all generated files are valid with varying subsets', () => {
|
|
|
380
380
|
'events.generated.ts',
|
|
381
381
|
'analytics.generated.ts',
|
|
382
382
|
'translations-fields.generated.ts',
|
|
383
|
+
'security-mfa-providers.generated.ts',
|
|
384
|
+
'security-sudo.generated.ts',
|
|
385
|
+
'frontend-middleware.generated.ts',
|
|
386
|
+
'backend-middleware.generated.ts',
|
|
383
387
|
]
|
|
384
388
|
for (const file of expectedFiles) {
|
|
385
389
|
const content = readGenerated(tmpDir, file)
|
|
@@ -433,6 +437,42 @@ describe('all generated files are valid with varying subsets', () => {
|
|
|
433
437
|
expect(aiTools).not.toContain('no_ai')
|
|
434
438
|
})
|
|
435
439
|
|
|
440
|
+
it('security generated registries are empty when no module provides security convention files', async () => {
|
|
441
|
+
scaffoldModule(tmpDir, 'no_security', 'pkg', ['setup.ts'])
|
|
442
|
+
const resolver = createMockResolver(tmpDir, [
|
|
443
|
+
{ id: 'no_security', from: '@open-mercato/core' },
|
|
444
|
+
])
|
|
445
|
+
await generateModuleRegistry({ resolver, quiet: true })
|
|
446
|
+
|
|
447
|
+
const mfaProviders = readGenerated(tmpDir, 'security-mfa-providers.generated.ts')!
|
|
448
|
+
const sudoTargets = readGenerated(tmpDir, 'security-sudo.generated.ts')!
|
|
449
|
+
|
|
450
|
+
expect(mfaProviders).toContain('export const securityMfaProviderEntries')
|
|
451
|
+
expect(mfaProviders).not.toContain('no_security')
|
|
452
|
+
expect(sudoTargets).toContain('export const securitySudoTargetEntries')
|
|
453
|
+
expect(sudoTargets).toContain('const entriesRaw: SecuritySudoTargetEntryRaw[] = [\n]')
|
|
454
|
+
expect(sudoTargets).not.toContain('no_security')
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('discovers security convention files into dedicated generated registries', async () => {
|
|
458
|
+
scaffoldModule(tmpDir, 'security_ext', 'pkg', [
|
|
459
|
+
'security.mfa-providers.ts',
|
|
460
|
+
'security.sudo.ts',
|
|
461
|
+
])
|
|
462
|
+
const resolver = createMockResolver(tmpDir, [
|
|
463
|
+
{ id: 'security_ext', from: '@open-mercato/core' },
|
|
464
|
+
])
|
|
465
|
+
await generateModuleRegistry({ resolver, quiet: true })
|
|
466
|
+
|
|
467
|
+
const mfaProviders = readGenerated(tmpDir, 'security-mfa-providers.generated.ts')!
|
|
468
|
+
const sudoTargets = readGenerated(tmpDir, 'security-sudo.generated.ts')!
|
|
469
|
+
|
|
470
|
+
expect(mfaProviders).toContain('security_ext')
|
|
471
|
+
expect(mfaProviders).toContain('mfaProviders')
|
|
472
|
+
expect(sudoTargets).toContain('security_ext')
|
|
473
|
+
expect(sudoTargets).toContain('sudoTargets')
|
|
474
|
+
})
|
|
475
|
+
|
|
436
476
|
it('notifications.generated.ts uses typed fallback for legacy "types" export', async () => {
|
|
437
477
|
scaffoldModule(tmpDir, 'notif_mod', 'pkg', ['notifications.ts'])
|
|
438
478
|
const resolver = createMockResolver(tmpDir, [
|
|
@@ -444,4 +484,20 @@ describe('all generated files are valid with varying subsets', () => {
|
|
|
444
484
|
expect(notifications).toContain('as any).types')
|
|
445
485
|
expect(notifications).toContain('as NotificationTypeDefinition[]')
|
|
446
486
|
})
|
|
487
|
+
|
|
488
|
+
it('discovers frontend and backend middleware conventions', async () => {
|
|
489
|
+
scaffoldModule(tmpDir, 'security', 'pkg', [
|
|
490
|
+
'frontend/middleware.ts',
|
|
491
|
+
'backend/middleware.ts',
|
|
492
|
+
])
|
|
493
|
+
const resolver = createMockResolver(tmpDir, [
|
|
494
|
+
{ id: 'security', from: '@open-mercato/core' },
|
|
495
|
+
])
|
|
496
|
+
await generateModuleRegistry({ resolver, quiet: true })
|
|
497
|
+
|
|
498
|
+
const frontendMiddleware = readGenerated(tmpDir, 'frontend-middleware.generated.ts')!
|
|
499
|
+
const backendMiddleware = readGenerated(tmpDir, 'backend-middleware.generated.ts')!
|
|
500
|
+
expect(frontendMiddleware).toContain("moduleId: 'security'")
|
|
501
|
+
expect(backendMiddleware).toContain("moduleId: 'security'")
|
|
502
|
+
})
|
|
447
503
|
})
|
|
@@ -339,7 +339,8 @@ describe('resolveModuleFile', () => {
|
|
|
339
339
|
const conventionFiles = [
|
|
340
340
|
'acl.ts', 'ce.ts', 'search.ts', 'notifications.ts',
|
|
341
341
|
'ai-tools.ts', 'events.ts', 'analytics.ts', 'setup.ts',
|
|
342
|
-
'translations.ts', '
|
|
342
|
+
'translations.ts', 'security.mfa-providers.ts', 'security.sudo.ts',
|
|
343
|
+
'data/extensions.ts', 'data/fields.ts',
|
|
343
344
|
]
|
|
344
345
|
for (const file of conventionFiles) {
|
|
345
346
|
touch(file, 'pkg')
|
|
@@ -407,6 +407,10 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
407
407
|
const analyticsChecksumFile = path.join(outputDir, 'analytics.generated.checksum')
|
|
408
408
|
const transFieldsOutFile = path.join(outputDir, 'translations-fields.generated.ts')
|
|
409
409
|
const transFieldsChecksumFile = path.join(outputDir, 'translations-fields.generated.checksum')
|
|
410
|
+
const securityMfaProvidersOutFile = path.join(outputDir, 'security-mfa-providers.generated.ts')
|
|
411
|
+
const securityMfaProvidersChecksumFile = path.join(outputDir, 'security-mfa-providers.generated.checksum')
|
|
412
|
+
const securitySudoOutFile = path.join(outputDir, 'security-sudo.generated.ts')
|
|
413
|
+
const securitySudoChecksumFile = path.join(outputDir, 'security-sudo.generated.checksum')
|
|
410
414
|
const enrichersOutFile = path.join(outputDir, 'enrichers.generated.ts')
|
|
411
415
|
const enrichersChecksumFile = path.join(outputDir, 'enrichers.generated.checksum')
|
|
412
416
|
const interceptorsOutFile = path.join(outputDir, 'interceptors.generated.ts')
|
|
@@ -419,6 +423,10 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
419
423
|
const guardsChecksumFile = path.join(outputDir, 'guards.generated.checksum')
|
|
420
424
|
const commandInterceptorsOutFile = path.join(outputDir, 'command-interceptors.generated.ts')
|
|
421
425
|
const commandInterceptorsChecksumFile = path.join(outputDir, 'command-interceptors.generated.checksum')
|
|
426
|
+
const frontendMiddlewareOutFile = path.join(outputDir, 'frontend-middleware.generated.ts')
|
|
427
|
+
const frontendMiddlewareChecksumFile = path.join(outputDir, 'frontend-middleware.generated.checksum')
|
|
428
|
+
const backendMiddlewareOutFile = path.join(outputDir, 'backend-middleware.generated.ts')
|
|
429
|
+
const backendMiddlewareChecksumFile = path.join(outputDir, 'backend-middleware.generated.checksum')
|
|
422
430
|
|
|
423
431
|
const enabled = resolver.loadEnabledModules()
|
|
424
432
|
const imports: string[] = []
|
|
@@ -450,6 +458,10 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
450
458
|
const analyticsImports: string[] = []
|
|
451
459
|
const transFieldsConfigs: string[] = []
|
|
452
460
|
const transFieldsImports: string[] = []
|
|
461
|
+
const securityMfaProviderConfigs: string[] = []
|
|
462
|
+
const securityMfaProviderImports: string[] = []
|
|
463
|
+
const securitySudoConfigs: string[] = []
|
|
464
|
+
const securitySudoImports: string[] = []
|
|
453
465
|
const enricherConfigs: string[] = []
|
|
454
466
|
const enricherImports: string[] = []
|
|
455
467
|
const interceptorConfigs: string[] = []
|
|
@@ -462,6 +474,10 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
462
474
|
const guardImports: string[] = []
|
|
463
475
|
const commandInterceptorConfigs: string[] = []
|
|
464
476
|
const commandInterceptorImports: string[] = []
|
|
477
|
+
const frontendMiddlewareConfigs: string[] = []
|
|
478
|
+
const frontendMiddlewareImports: string[] = []
|
|
479
|
+
const backendMiddlewareConfigs: string[] = []
|
|
480
|
+
const backendMiddlewareImports: string[] = []
|
|
465
481
|
|
|
466
482
|
// UMES conflict detection: collect file paths during module processing
|
|
467
483
|
const umesConflictSources: Array<{
|
|
@@ -735,6 +751,24 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
735
751
|
configExpr: (n, id) => `{ moduleId: '${id}', fields: (${n}.default ?? ${n}.translatableFields ?? {}) as Record<string, string[]> }`,
|
|
736
752
|
})
|
|
737
753
|
|
|
754
|
+
processStandaloneConfig({
|
|
755
|
+
roots, imps, modId, importIdRef,
|
|
756
|
+
relativePath: 'security.mfa-providers.ts',
|
|
757
|
+
prefix: 'SECURITY_MFA_PROVIDERS',
|
|
758
|
+
standaloneImports: securityMfaProviderImports,
|
|
759
|
+
standaloneConfigs: securityMfaProviderConfigs,
|
|
760
|
+
configExpr: (n, id) => `{ moduleId: '${id}', providers: ((${n}.default ?? ${n}.mfaProviders ?? []) as unknown[]) }`,
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
processStandaloneConfig({
|
|
764
|
+
roots, imps, modId, importIdRef,
|
|
765
|
+
relativePath: 'security.sudo.ts',
|
|
766
|
+
prefix: 'SECURITY_SUDO',
|
|
767
|
+
standaloneImports: securitySudoImports,
|
|
768
|
+
standaloneConfigs: securitySudoConfigs,
|
|
769
|
+
configExpr: (n, id) => `{ moduleId: '${id}', targets: ((${n}.default ?? ${n}.sudoTargets ?? []) as Array<Record<string, unknown>>) }`,
|
|
770
|
+
})
|
|
771
|
+
|
|
738
772
|
// Inbox Actions: inbox-actions.ts
|
|
739
773
|
{
|
|
740
774
|
const resolved = resolveModuleFile(roots, imps, 'inbox-actions.ts')
|
|
@@ -768,6 +802,26 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
768
802
|
configExpr: (n, id) => `{ moduleId: '${id}', interceptors: ((${n} as any).interceptors ?? (${n} as any).default ?? []) }`,
|
|
769
803
|
})
|
|
770
804
|
|
|
805
|
+
// 10g. Frontend page middleware: frontend/middleware.ts
|
|
806
|
+
processStandaloneConfig({
|
|
807
|
+
roots, imps, modId, importIdRef,
|
|
808
|
+
relativePath: 'frontend/middleware.ts',
|
|
809
|
+
prefix: 'FRONTEND_MIDDLEWARE',
|
|
810
|
+
standaloneImports: frontendMiddlewareImports,
|
|
811
|
+
standaloneConfigs: frontendMiddlewareConfigs,
|
|
812
|
+
configExpr: (n, id) => `{ moduleId: '${id}', middleware: ((${n} as any).middleware ?? (${n} as any).default ?? []) }`,
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
// 10h. Backend page middleware: backend/middleware.ts
|
|
816
|
+
processStandaloneConfig({
|
|
817
|
+
roots, imps, modId, importIdRef,
|
|
818
|
+
relativePath: 'backend/middleware.ts',
|
|
819
|
+
prefix: 'BACKEND_MIDDLEWARE',
|
|
820
|
+
standaloneImports: backendMiddlewareImports,
|
|
821
|
+
standaloneConfigs: backendMiddlewareConfigs,
|
|
822
|
+
configExpr: (n, id) => `{ moduleId: '${id}', middleware: ((${n} as any).middleware ?? (${n} as any).default ?? []) }`,
|
|
823
|
+
})
|
|
824
|
+
|
|
771
825
|
// 11. Setup: setup.ts
|
|
772
826
|
{
|
|
773
827
|
const setup = resolveConventionFile(roots, imps, 'setup.ts', 'SETUP', modId, importIdRef, imports)
|
|
@@ -1151,6 +1205,31 @@ export const allTranslatableEntityTypes = Object.keys(allFields)
|
|
|
1151
1205
|
|
|
1152
1206
|
// Auto-register on import (side-effect)
|
|
1153
1207
|
registerTranslatableFields(allFields)
|
|
1208
|
+
`
|
|
1209
|
+
|
|
1210
|
+
const securityMfaProviderEntriesLiteral = securityMfaProviderConfigs.join(',\n ')
|
|
1211
|
+
const securityMfaProviderImportSection = securityMfaProviderImports.join('\n')
|
|
1212
|
+
const securityMfaProvidersOutput = `// AUTO-GENERATED by mercato generate registry
|
|
1213
|
+
${securityMfaProviderImportSection ? `${securityMfaProviderImportSection}\n` : ''}type SecurityMfaProviderEntry = { moduleId: string; providers: unknown[] }
|
|
1214
|
+
|
|
1215
|
+
export const securityMfaProviderEntries: SecurityMfaProviderEntry[] = [
|
|
1216
|
+
${securityMfaProviderEntriesLiteral ? ` ${securityMfaProviderEntriesLiteral}\n` : ''}]
|
|
1217
|
+
`
|
|
1218
|
+
|
|
1219
|
+
const securitySudoEntriesLiteral = securitySudoConfigs.join(',\n ')
|
|
1220
|
+
const securitySudoImportSection = securitySudoImports.join('\n')
|
|
1221
|
+
const securitySudoOutput = `// AUTO-GENERATED by mercato generate registry
|
|
1222
|
+
import type { SecuritySudoTarget, SecuritySudoTargetEntry } from '@open-mercato/enterprise/modules/security'
|
|
1223
|
+
${securitySudoImportSection ? `\n${securitySudoImportSection}\n` : '\n'}
|
|
1224
|
+
type SecuritySudoTargetEntryRaw = { moduleId: string; targets: Array<Record<string, unknown>> }
|
|
1225
|
+
|
|
1226
|
+
const entriesRaw: SecuritySudoTargetEntryRaw[] = [
|
|
1227
|
+
${securitySudoEntriesLiteral ? ` ${securitySudoEntriesLiteral}\n` : ''}]
|
|
1228
|
+
|
|
1229
|
+
export const securitySudoTargetEntries: SecuritySudoTargetEntry[] = entriesRaw.map((entry) => ({
|
|
1230
|
+
moduleId: entry.moduleId,
|
|
1231
|
+
targets: entry.targets as SecuritySudoTarget[],
|
|
1232
|
+
}))
|
|
1154
1233
|
`
|
|
1155
1234
|
|
|
1156
1235
|
const notificationEntriesLiteral = notificationTypes.join(',\n ')
|
|
@@ -1429,6 +1508,8 @@ export const allAiTools = aiToolConfigEntries.flatMap(e => e.tools)
|
|
|
1429
1508
|
writeGeneratedFile({ outFile: eventsOutFile, checksumFile: eventsChecksumFile, content: eventsOutput, structureChecksum, result, quiet })
|
|
1430
1509
|
writeGeneratedFile({ outFile: analyticsOutFile, checksumFile: analyticsChecksumFile, content: analyticsOutput, structureChecksum, result, quiet })
|
|
1431
1510
|
writeGeneratedFile({ outFile: transFieldsOutFile, checksumFile: transFieldsChecksumFile, content: transFieldsOutput, structureChecksum, result, quiet })
|
|
1511
|
+
writeGeneratedFile({ outFile: securityMfaProvidersOutFile, checksumFile: securityMfaProvidersChecksumFile, content: securityMfaProvidersOutput, structureChecksum, result, quiet })
|
|
1512
|
+
writeGeneratedFile({ outFile: securitySudoOutFile, checksumFile: securitySudoChecksumFile, content: securitySudoOutput, structureChecksum, result, quiet })
|
|
1432
1513
|
|
|
1433
1514
|
// Enrichers generated file
|
|
1434
1515
|
const enricherEntriesLiteral = enricherConfigs.join(',\n ')
|
|
@@ -1518,6 +1599,46 @@ ${commandInterceptorEntriesLiteral ? ` ${commandInterceptorEntriesLiteral}\n` :
|
|
|
1518
1599
|
`
|
|
1519
1600
|
writeGeneratedFile({ outFile: commandInterceptorsOutFile, checksumFile: commandInterceptorsChecksumFile, content: commandInterceptorsOutput, structureChecksum, result, quiet })
|
|
1520
1601
|
|
|
1602
|
+
const frontendMiddlewareEntriesLiteral = frontendMiddlewareConfigs.join(',\n ')
|
|
1603
|
+
const frontendMiddlewareImportSection = frontendMiddlewareImports.join('\n')
|
|
1604
|
+
const frontendMiddlewareOutput = `// AUTO-GENERATED by mercato generate registry
|
|
1605
|
+
import type { PageMiddlewareRegistryEntry, PageRouteMiddleware } from '@open-mercato/shared/modules/middleware/page'
|
|
1606
|
+
${frontendMiddlewareImportSection ? `\n${frontendMiddlewareImportSection}\n` : '\n'}type FrontendMiddlewareEntry = { moduleId: string; middleware: PageRouteMiddleware[] }
|
|
1607
|
+
|
|
1608
|
+
const entriesRaw: FrontendMiddlewareEntry[] = [
|
|
1609
|
+
${frontendMiddlewareEntriesLiteral ? ` ${frontendMiddlewareEntriesLiteral}\n` : ''}]
|
|
1610
|
+
|
|
1611
|
+
export const frontendMiddlewareEntries: PageMiddlewareRegistryEntry[] = entriesRaw
|
|
1612
|
+
`
|
|
1613
|
+
writeGeneratedFile({
|
|
1614
|
+
outFile: frontendMiddlewareOutFile,
|
|
1615
|
+
checksumFile: frontendMiddlewareChecksumFile,
|
|
1616
|
+
content: frontendMiddlewareOutput,
|
|
1617
|
+
structureChecksum,
|
|
1618
|
+
result,
|
|
1619
|
+
quiet,
|
|
1620
|
+
})
|
|
1621
|
+
|
|
1622
|
+
const backendMiddlewareEntriesLiteral = backendMiddlewareConfigs.join(',\n ')
|
|
1623
|
+
const backendMiddlewareImportSection = backendMiddlewareImports.join('\n')
|
|
1624
|
+
const backendMiddlewareOutput = `// AUTO-GENERATED by mercato generate registry
|
|
1625
|
+
import type { PageMiddlewareRegistryEntry, PageRouteMiddleware } from '@open-mercato/shared/modules/middleware/page'
|
|
1626
|
+
${backendMiddlewareImportSection ? `\n${backendMiddlewareImportSection}\n` : '\n'}type BackendMiddlewareEntry = { moduleId: string; middleware: PageRouteMiddleware[] }
|
|
1627
|
+
|
|
1628
|
+
const entriesRaw: BackendMiddlewareEntry[] = [
|
|
1629
|
+
${backendMiddlewareEntriesLiteral ? ` ${backendMiddlewareEntriesLiteral}\n` : ''}]
|
|
1630
|
+
|
|
1631
|
+
export const backendMiddlewareEntries: PageMiddlewareRegistryEntry[] = entriesRaw
|
|
1632
|
+
`
|
|
1633
|
+
writeGeneratedFile({
|
|
1634
|
+
outFile: backendMiddlewareOutFile,
|
|
1635
|
+
checksumFile: backendMiddlewareChecksumFile,
|
|
1636
|
+
content: backendMiddlewareOutput,
|
|
1637
|
+
structureChecksum,
|
|
1638
|
+
result,
|
|
1639
|
+
quiet,
|
|
1640
|
+
})
|
|
1641
|
+
|
|
1521
1642
|
return result
|
|
1522
1643
|
}
|
|
1523
1644
|
|
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
readEphemeralEnvironmentState,
|
|
15
15
|
clearEphemeralEnvironmentState,
|
|
16
16
|
resolveBuildCacheTtlSeconds,
|
|
17
|
+
resolveAppReadyTimeoutMs,
|
|
17
18
|
shouldReuseBuildArtifacts,
|
|
18
19
|
} from '../integration'
|
|
19
20
|
|
|
20
21
|
const CACHE_TTL_ENV_VAR = 'OM_INTEGRATION_BUILD_CACHE_TTL_SECONDS'
|
|
22
|
+
const APP_READY_TIMEOUT_ENV_VAR = 'OM_INTEGRATION_APP_READY_TIMEOUT_SECONDS'
|
|
21
23
|
const resolver = createResolver()
|
|
22
24
|
const projectRootDirectory = resolver.getRootDir()
|
|
23
25
|
|
|
@@ -36,6 +38,7 @@ describe('integration cache and options', () => {
|
|
|
36
38
|
const ephemeralEnvFilePath = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-env.json')
|
|
37
39
|
const ephemeralLegacyEnvFilePath = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-env.md')
|
|
38
40
|
const originalCacheTtl = process.env[CACHE_TTL_ENV_VAR]
|
|
41
|
+
const originalAppReadyTimeout = process.env[APP_READY_TIMEOUT_ENV_VAR]
|
|
39
42
|
let originalEphemeralEnvState: string | null = null
|
|
40
43
|
let originalEphemeralLegacyEnvState: string | null = null
|
|
41
44
|
|
|
@@ -61,6 +64,11 @@ describe('integration cache and options', () => {
|
|
|
61
64
|
} else {
|
|
62
65
|
process.env[CACHE_TTL_ENV_VAR] = originalCacheTtl
|
|
63
66
|
}
|
|
67
|
+
if (originalAppReadyTimeout === undefined) {
|
|
68
|
+
delete process.env[APP_READY_TIMEOUT_ENV_VAR]
|
|
69
|
+
} else {
|
|
70
|
+
process.env[APP_READY_TIMEOUT_ENV_VAR] = originalAppReadyTimeout
|
|
71
|
+
}
|
|
64
72
|
await restoreEphemeralStateFiles(originalEphemeralEnvState, originalEphemeralLegacyEnvState)
|
|
65
73
|
})
|
|
66
74
|
|
|
@@ -68,6 +76,9 @@ describe('integration cache and options', () => {
|
|
|
68
76
|
const baseUrl = 'http://127.0.0.1:5001'
|
|
69
77
|
const fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(async (input) => {
|
|
70
78
|
const url = typeof input === 'string' ? input : String(input)
|
|
79
|
+
if (url.endsWith('/api/auth/login')) {
|
|
80
|
+
return { status: 401, text: async () => '' } as unknown as Response
|
|
81
|
+
}
|
|
71
82
|
if (url.endsWith('/login')) {
|
|
72
83
|
return {
|
|
73
84
|
status: 200,
|
|
@@ -77,9 +88,6 @@ describe('integration cache and options', () => {
|
|
|
77
88
|
if (url.includes('/_next/static/chunks/app-healthcheck.js')) {
|
|
78
89
|
return { status: 200, text: async () => '' } as unknown as Response
|
|
79
90
|
}
|
|
80
|
-
if (url.endsWith('/api/auth/login')) {
|
|
81
|
-
return { status: 401, text: async () => '' } as unknown as Response
|
|
82
|
-
}
|
|
83
91
|
return { status: 200, text: async () => '' } as unknown as Response
|
|
84
92
|
})
|
|
85
93
|
|
|
@@ -113,6 +121,87 @@ describe('integration cache and options', () => {
|
|
|
113
121
|
}
|
|
114
122
|
}, 20000)
|
|
115
123
|
|
|
124
|
+
it('reuses an existing environment when /login returns a redirect status other than 302', async () => {
|
|
125
|
+
const baseUrl = 'http://127.0.0.1:5001'
|
|
126
|
+
const fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(async (input) => {
|
|
127
|
+
const url = typeof input === 'string' ? input : String(input)
|
|
128
|
+
if (url.endsWith('/api/auth/login')) {
|
|
129
|
+
return { status: 401, text: async () => '' } as unknown as Response
|
|
130
|
+
}
|
|
131
|
+
if (url.endsWith('/login')) {
|
|
132
|
+
return { status: 308, text: async () => '' } as unknown as Response
|
|
133
|
+
}
|
|
134
|
+
return { status: 200, text: async () => '' } as unknown as Response
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await writeEphemeralEnvironmentState({
|
|
139
|
+
baseUrl,
|
|
140
|
+
port: 5001,
|
|
141
|
+
logPrefix: 'integration',
|
|
142
|
+
captureScreenshots: true,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const environment = await tryReuseExistingEnvironment({
|
|
146
|
+
verbose: false,
|
|
147
|
+
captureScreenshots: true,
|
|
148
|
+
logPrefix: 'integration',
|
|
149
|
+
forceRebuild: false,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(environment).not.toBeNull()
|
|
153
|
+
expect(environment).toMatchObject({
|
|
154
|
+
baseUrl,
|
|
155
|
+
port: 5001,
|
|
156
|
+
ownedByCurrentProcess: false,
|
|
157
|
+
})
|
|
158
|
+
} finally {
|
|
159
|
+
fetchSpy.mockRestore()
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('reuses an existing environment when /login returns healthy HTML without static asset references', async () => {
|
|
164
|
+
const baseUrl = 'http://127.0.0.1:5001'
|
|
165
|
+
const fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(async (input) => {
|
|
166
|
+
const url = typeof input === 'string' ? input : String(input)
|
|
167
|
+
if (url.endsWith('/api/auth/login')) {
|
|
168
|
+
return { status: 401, text: async () => '' } as unknown as Response
|
|
169
|
+
}
|
|
170
|
+
if (url.endsWith('/login')) {
|
|
171
|
+
return {
|
|
172
|
+
status: 200,
|
|
173
|
+
text: async () => '<!doctype html><html><body><form data-auth-ready="0"></form></body></html>',
|
|
174
|
+
} as unknown as Response
|
|
175
|
+
}
|
|
176
|
+
return { status: 200, text: async () => '' } as unknown as Response
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await writeEphemeralEnvironmentState({
|
|
181
|
+
baseUrl,
|
|
182
|
+
port: 5001,
|
|
183
|
+
logPrefix: 'integration',
|
|
184
|
+
captureScreenshots: false,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const environment = await tryReuseExistingEnvironment({
|
|
188
|
+
verbose: false,
|
|
189
|
+
captureScreenshots: false,
|
|
190
|
+
logPrefix: 'integration',
|
|
191
|
+
forceRebuild: false,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
expect(environment).not.toBeNull()
|
|
195
|
+
expect(environment).toMatchObject({
|
|
196
|
+
baseUrl,
|
|
197
|
+
port: 5001,
|
|
198
|
+
ownedByCurrentProcess: false,
|
|
199
|
+
})
|
|
200
|
+
} finally {
|
|
201
|
+
fetchSpy.mockRestore()
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
116
205
|
it('falls back to rebuilding when the ephemeral environment state is unreachable', async () => {
|
|
117
206
|
const baseUrl = 'http://127.0.0.1:5001'
|
|
118
207
|
const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({ status: 500 } as unknown as Response)
|
|
@@ -223,6 +312,20 @@ describe('integration cache and options', () => {
|
|
|
223
312
|
warn.mockRestore()
|
|
224
313
|
})
|
|
225
314
|
|
|
315
|
+
it('resolves app readiness timeout from env variable', () => {
|
|
316
|
+
delete process.env[APP_READY_TIMEOUT_ENV_VAR]
|
|
317
|
+
expect(resolveAppReadyTimeoutMs('integration')).toBe(90_000)
|
|
318
|
+
|
|
319
|
+
process.env[APP_READY_TIMEOUT_ENV_VAR] = '180'
|
|
320
|
+
expect(resolveAppReadyTimeoutMs('integration')).toBe(180_000)
|
|
321
|
+
|
|
322
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
323
|
+
process.env[APP_READY_TIMEOUT_ENV_VAR] = '0'
|
|
324
|
+
expect(resolveAppReadyTimeoutMs('integration')).toBe(90_000)
|
|
325
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Invalid'))
|
|
326
|
+
warn.mockRestore()
|
|
327
|
+
})
|
|
328
|
+
|
|
226
329
|
it('reuses build artifacts only with matching source fingerprint and fresh cache state', async () => {
|
|
227
330
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'om-int-cache-test-'))
|
|
228
331
|
try {
|