@open-mercato/cli 0.10.1-develop.7990.1.82d0a183c5 → 0.10.1-develop.8004.1.a8a79ee43f

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 (182) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/build.mjs +1 -0
  3. package/dist/agentic/guides/module-facts.json +162 -162
  4. package/dist/agentic/guides/module-facts.v2.json +162 -162
  5. package/dist/agentic/guides/module-tool.md +21 -0
  6. package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
  7. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  8. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  9. package/dist/agentic/guides/modules/api_keys/index.md +1 -1
  10. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  11. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  12. package/dist/agentic/guides/modules/auth/index.md +1 -1
  13. package/dist/agentic/guides/modules/availability/index.md +1 -1
  14. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  15. package/dist/agentic/guides/modules/carrier_inpost/index.md +1 -1
  16. package/dist/agentic/guides/modules/cart/index.md +1 -1
  17. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  18. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  19. package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
  20. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  21. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  22. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  23. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  24. package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
  25. package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
  26. package/dist/agentic/guides/modules/channel_smtp/index.md +1 -1
  27. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  28. package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
  29. package/dist/agentic/guides/modules/configs/index.md +1 -1
  30. package/dist/agentic/guides/modules/content/index.md +1 -1
  31. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  32. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  33. package/dist/agentic/guides/modules/customer_groups/index.md +1 -1
  34. package/dist/agentic/guides/modules/customers/index.md +1 -1
  35. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  36. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  37. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  38. package/dist/agentic/guides/modules/devices/index.md +1 -1
  39. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  40. package/dist/agentic/guides/modules/directory/index.md +1 -1
  41. package/dist/agentic/guides/modules/documents/index.md +1 -1
  42. package/dist/agentic/guides/modules/ecommerce/index.md +1 -1
  43. package/dist/agentic/guides/modules/entities/index.md +1 -1
  44. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  45. package/dist/agentic/guides/modules/events/index.md +1 -1
  46. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  47. package/dist/agentic/guides/modules/forms/index.md +1 -1
  48. package/dist/agentic/guides/modules/gateway_autopay/index.md +1 -1
  49. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  50. package/dist/agentic/guides/modules/gateway_tpay/index.md +1 -1
  51. package/dist/agentic/guides/modules/generators/index.md +1 -1
  52. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  53. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  54. package/dist/agentic/guides/modules/messages/index.md +1 -1
  55. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  56. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  57. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  58. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  59. package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
  60. package/dist/agentic/guides/modules/planner/index.md +1 -1
  61. package/dist/agentic/guides/modules/portal/index.md +1 -1
  62. package/dist/agentic/guides/modules/progress/index.md +1 -1
  63. package/dist/agentic/guides/modules/promotions/index.md +1 -1
  64. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  65. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  66. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  67. package/dist/agentic/guides/modules/resources/index.md +1 -1
  68. package/dist/agentic/guides/modules/sales/index.md +1 -1
  69. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  70. package/dist/agentic/guides/modules/search/index.md +1 -1
  71. package/dist/agentic/guides/modules/security/index.md +1 -1
  72. package/dist/agentic/guides/modules/seeds/index.md +1 -1
  73. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  74. package/dist/agentic/guides/modules/sso/index.md +1 -1
  75. package/dist/agentic/guides/modules/staff/index.md +1 -1
  76. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  77. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  78. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  79. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  80. package/dist/agentic/guides/modules/telemetry/index.md +1 -1
  81. package/dist/agentic/guides/modules/tillio/index.md +1 -1
  82. package/dist/agentic/guides/modules/translations/index.md +1 -1
  83. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  84. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  85. package/dist/agentic/guides/modules/wms/index.md +1 -1
  86. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  87. package/dist/agentic/guides/reference-module-facts.json +1 -1
  88. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +4 -0
  89. package/dist/agentic/guides/upstream/manifest.json +2 -2
  90. package/dist/agentic/shared/ai/harness/cases.json +23 -9
  91. package/dist/agentic/shared/ai/harness/source-link-inventory.json +90 -4
  92. package/dist/i18n/de.json +4 -0
  93. package/dist/i18n/en.json +4 -0
  94. package/dist/i18n/es.json +4 -0
  95. package/dist/i18n/ko.json +4 -0
  96. package/dist/i18n/pl.json +4 -0
  97. package/dist/lib/__fixtures__/module-search/built-in.json +235 -0
  98. package/dist/lib/__fixtures__/module-search/catalog-parity.json +4076 -0
  99. package/dist/lib/__fixtures__/module-search/modules-index.json +1980 -0
  100. package/dist/lib/__fixtures__/official-module-package/package.json +33 -0
  101. package/dist/lib/__integration__/TC-INT-007.spec.js +207 -19
  102. package/dist/lib/__integration__/TC-INT-007.spec.js.map +2 -2
  103. package/dist/lib/__integration__/TC-INT-009.spec.js +267 -0
  104. package/dist/lib/__integration__/TC-INT-009.spec.js.map +7 -0
  105. package/dist/lib/eject.js +25 -9
  106. package/dist/lib/eject.js.map +2 -2
  107. package/dist/lib/generators/entity-ids.js +2 -0
  108. package/dist/lib/generators/entity-ids.js.map +2 -2
  109. package/dist/lib/generators/module-di.js +2 -0
  110. package/dist/lib/generators/module-di.js.map +2 -2
  111. package/dist/lib/generators/module-entities.js +2 -0
  112. package/dist/lib/generators/module-entities.js.map +2 -2
  113. package/dist/lib/generators/module-package-sources.js +6 -4
  114. package/dist/lib/generators/module-package-sources.js.map +2 -2
  115. package/dist/lib/generators/module-registry.js +32 -86
  116. package/dist/lib/generators/module-registry.js.map +2 -2
  117. package/dist/lib/generators/openapi.js +2 -0
  118. package/dist/lib/generators/openapi.js.map +2 -2
  119. package/dist/lib/generators/source-exports.js +73 -0
  120. package/dist/lib/generators/source-exports.js.map +7 -0
  121. package/dist/lib/init-secrets.js +33 -1
  122. package/dist/lib/init-secrets.js.map +2 -2
  123. package/dist/lib/init-summary.js +44 -0
  124. package/dist/lib/init-summary.js.map +7 -0
  125. package/dist/lib/module-github.js +98 -0
  126. package/dist/lib/module-github.js.map +7 -0
  127. package/dist/lib/module-identity.js +131 -0
  128. package/dist/lib/module-identity.js.map +7 -0
  129. package/dist/lib/module-inspection.js +124 -0
  130. package/dist/lib/module-inspection.js.map +7 -0
  131. package/dist/lib/module-install.js +150 -21
  132. package/dist/lib/module-install.js.map +3 -3
  133. package/dist/lib/module-package.js +205 -15
  134. package/dist/lib/module-package.js.map +2 -2
  135. package/dist/lib/modules-config.js +69 -30
  136. package/dist/lib/modules-config.js.map +2 -2
  137. package/dist/lib/resolver.js +33 -34
  138. package/dist/lib/resolver.js.map +2 -2
  139. package/dist/mercato.js +17 -37
  140. package/dist/mercato.js.map +2 -2
  141. package/package.json +7 -7
  142. package/src/__tests__/mercato.test.ts +71 -4
  143. package/src/i18n/de.json +4 -0
  144. package/src/i18n/en.json +4 -0
  145. package/src/i18n/es.json +4 -0
  146. package/src/i18n/ko.json +4 -0
  147. package/src/i18n/pl.json +4 -0
  148. package/src/lib/__fixtures__/official-module-package/dist/modules/test_package/widgets/injection/test/widget.js +1 -0
  149. package/src/lib/__integration__/TC-INT-007.spec.ts +219 -19
  150. package/src/lib/__integration__/TC-INT-009.spec.ts +279 -0
  151. package/src/lib/__tests__/eject.test.ts +98 -4
  152. package/src/lib/__tests__/init-secrets.test.ts +85 -1
  153. package/src/lib/__tests__/init-summary.test.ts +50 -0
  154. package/src/lib/__tests__/module-github.test.ts +108 -0
  155. package/src/lib/__tests__/module-identity.test.ts +144 -0
  156. package/src/lib/__tests__/module-inspection.test.ts +195 -0
  157. package/src/lib/__tests__/module-install.test.ts +399 -10
  158. package/src/lib/__tests__/module-package.test.ts +154 -0
  159. package/src/lib/__tests__/modules-config.test.ts +55 -0
  160. package/src/lib/__tests__/resolver.packages.test.ts +29 -0
  161. package/src/lib/eject.ts +28 -10
  162. package/src/lib/generators/__tests__/module-id-collisions.test.ts +146 -0
  163. package/src/lib/generators/__tests__/module-package-sources.test.ts +19 -2
  164. package/src/lib/generators/__tests__/module-requirements.test.ts +56 -0
  165. package/src/lib/generators/entity-ids.ts +2 -0
  166. package/src/lib/generators/module-di.ts +2 -0
  167. package/src/lib/generators/module-entities.ts +2 -0
  168. package/src/lib/generators/module-package-sources.ts +7 -5
  169. package/src/lib/generators/module-registry.ts +33 -115
  170. package/src/lib/generators/openapi.ts +3 -0
  171. package/src/lib/generators/source-exports.ts +93 -0
  172. package/src/lib/init-secrets.ts +37 -0
  173. package/src/lib/init-summary.ts +54 -0
  174. package/src/lib/module-github.ts +109 -0
  175. package/src/lib/module-identity.ts +145 -0
  176. package/src/lib/module-inspection.ts +133 -0
  177. package/src/lib/module-install.ts +170 -22
  178. package/src/lib/module-package.ts +243 -16
  179. package/src/lib/modules-config.ts +80 -32
  180. package/src/lib/resolver.ts +35 -39
  181. package/src/mercato.ts +18 -44
  182. package/src/types/cross-spawn.d.ts +2 -2
@@ -0,0 +1,279 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { execFileSync, spawnSync } from 'node:child_process'
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..', '..')
9
+ const cliBin = path.join(repoRoot, 'packages', 'cli', 'dist', 'bin.js')
10
+
11
+ function writeFile(filePath: string, content: string): void {
12
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
13
+ fs.writeFileSync(filePath, content)
14
+ }
15
+
16
+ function createPackage(packageRoot: string, packageName: string, moduleIds: string[]): void {
17
+ writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({ name: packageName, version: '0.4.7', type: 'module' }))
18
+ for (const moduleId of moduleIds) {
19
+ writeFile(path.join(packageRoot, 'src', 'modules', moduleId, 'index.ts'), `export const metadata = { id: '${moduleId}', title: '${moduleId}', ejectable: true }\n`)
20
+ writeFile(path.join(packageRoot, 'dist', 'modules', moduleId, 'index.js'), `export const metadata = { id: '${moduleId}', title: '${moduleId}', ejectable: true }\n`)
21
+ writeFile(path.join(packageRoot, 'src', 'modules', moduleId, 'di.ts'), 'export function register() {}\n')
22
+ writeFile(path.join(packageRoot, 'dist', 'modules', moduleId, 'di.js'), 'export function register() {}\n')
23
+ }
24
+ }
25
+
26
+ function createApp(appDir: string, dependencies: Record<string, string> = {}): void {
27
+ writeFile(path.join(appDir, 'package.json'), JSON.stringify({ name: 'module-collision-integration', private: true, dependencies: { '@open-mercato/core': '0.4.7', ...dependencies } }))
28
+ writeFile(path.join(appDir, 'yarn.lock'), '')
29
+ writeFile(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
30
+ createPackage(path.join(appDir, 'node_modules', '@open-mercato', 'core'), '@open-mercato/core', [])
31
+ }
32
+
33
+ function runMercato(args: string[], appDir: string, environment: NodeJS.ProcessEnv = {}): { status: number | null; output: string } {
34
+ const result = spawnSync(process.execPath, [cliBin, ...args], {
35
+ cwd: appDir,
36
+ encoding: 'utf8',
37
+ env: { ...process.env, FORCE_COLOR: '0', NODE_NO_WARNINGS: '1', ...environment },
38
+ })
39
+ if (result.error) throw result.error
40
+ return { status: result.status, output: `${result.stdout}\n${result.stderr}` }
41
+ }
42
+
43
+ function installCommandStubs(appDir: string, candidateName?: string, candidateModuleId?: string): NodeJS.ProcessEnv {
44
+ const executableDir = path.join(appDir, 'bin')
45
+ const marker = path.join(appDir, 'yarn-invoked')
46
+ const yarnPath = path.join(executableDir, process.platform === 'win32' ? 'yarn.cmd' : 'yarn')
47
+ const yarnScript = `require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'invoked'); process.exit(1)\n`
48
+ writeFile(yarnPath, process.platform === 'win32'
49
+ ? `@"${process.execPath}" "%~dp0yarn-stub.cjs" %*\r\n`
50
+ : `#!${process.execPath}\n${yarnScript}`)
51
+ writeFile(path.join(executableDir, 'yarn-stub.cjs'), yarnScript)
52
+ fs.chmodSync(yarnPath, 0o755)
53
+ if (candidateName && candidateModuleId) {
54
+ const manifest = Buffer.from(JSON.stringify({ name: candidateName })).toString('base64')
55
+ const ghScript = `const endpoint = process.argv[3] || ''
56
+ const value = endpoint === 'user' ? { login: 'candidate' }
57
+ : endpoint.includes('/commits/') ? { sha: 'a'.repeat(40) }
58
+ : endpoint.includes('/contents/') ? { encoding: 'base64', content: ${JSON.stringify(manifest)} }
59
+ : endpoint.includes('/git/trees/') ? { tree: [{ path: 'src/modules/${candidateModuleId}/index.ts' }, { path: 'dist/modules/${candidateModuleId}/index.js' }] }
60
+ : { default_branch: 'main' }
61
+ process.stdout.write(JSON.stringify(value))\n`
62
+ const ghPath = path.join(executableDir, process.platform === 'win32' ? 'gh.cmd' : 'gh')
63
+ writeFile(ghPath, process.platform === 'win32'
64
+ ? `@"${process.execPath}" "%~dp0gh-stub.cjs" %*\r\n`
65
+ : `#!${process.execPath}\n${ghScript}`)
66
+ writeFile(path.join(executableDir, 'gh-stub.cjs'), ghScript)
67
+ fs.chmodSync(ghPath, 0o755)
68
+ }
69
+ return { PATH: executableDir + path.delimiter + process.env.PATH }
70
+ }
71
+
72
+ function snapshotDirectory(directory: string): Record<string, string> {
73
+ if (!fs.existsSync(directory)) return {}
74
+ const files: Record<string, string> = {}
75
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
76
+ const fullPath = path.join(directory, entry.name)
77
+ if (entry.isDirectory()) {
78
+ for (const [relativePath, content] of Object.entries(snapshotDirectory(fullPath))) files[path.join(entry.name, relativePath)] = content
79
+ } else {
80
+ files[entry.name] = fs.readFileSync(fullPath).toString('base64')
81
+ }
82
+ }
83
+ return files
84
+ }
85
+
86
+ test.describe('TC-INT-009: CLI source collision lifecycle safeguards', () => {
87
+ for (const kind of ['directory', 'archive']) {
88
+ test(`rejects a local ${kind} with unknown provenance before replacing an active npm binding`, () => {
89
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-file-origin-'))
90
+ const appDir = path.join(root, 'app')
91
+ try {
92
+ const binding = '@publisher/consents'
93
+ createApp(appDir, { [binding]: '0.4.7' })
94
+ createPackage(path.join(appDir, 'node_modules', binding), binding, ['consents'])
95
+ const packageRoot = path.join(root, 'package')
96
+ createPackage(packageRoot, binding, ['consents'])
97
+ if (kind === 'archive') execFileSync('tar', ['-czf', path.join(root, 'candidate.tgz'), '-C', root, 'package'])
98
+ const modulesSource = `export const enabledModules = [{ id: 'consents', from: '${binding}', locator: 'npm:${binding}' }]\n`
99
+ writeFile(path.join(appDir, 'src', 'modules.ts'), modulesSource)
100
+ const manifestBefore = fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')
101
+ const installedBefore = snapshotDirectory(path.join(appDir, 'node_modules'))
102
+ const environment = installCommandStubs(appDir)
103
+ const locator = kind === 'archive' ? 'file:../candidate.tgz' : 'file:../package'
104
+ const result = runMercato(['module', 'add', `${binding}@${locator}`, '--eject', '--allow-third-party'], appDir, environment)
105
+
106
+ expect(result.status, result.output).not.toBe(0)
107
+ expect(result.output).toContain('Package binding collision')
108
+ expect(result.output).toContain('unknown origin')
109
+ expect(result.output).toContain('consents')
110
+ expect(fs.existsSync(path.join(appDir, 'yarn-invoked'))).toBe(false)
111
+ expect(fs.readFileSync(path.join(appDir, 'src', 'modules.ts'), 'utf8')).toBe(modulesSource)
112
+ expect(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')).toBe(manifestBefore)
113
+ expect(snapshotDirectory(path.join(appDir, 'node_modules'))).toEqual(installedBefore)
114
+ expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'consents'))).toBe(false)
115
+ expect(fs.existsSync(path.join(appDir, '.mercato', 'generated'))).toBe(false)
116
+ } finally { fs.rmSync(root, { recursive: true, force: true }) }
117
+ })
118
+ }
119
+
120
+ test('rejects a GitHub replacement of an active binding even when the selected module ID differs', () => {
121
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-binding-collision-'))
122
+ try {
123
+ const binding = '@publisher/shared-package'
124
+ createApp(appDir, { [binding]: 'github:existing/provider#main' })
125
+ createPackage(path.join(appDir, 'node_modules', binding), binding, ['active_module'])
126
+ const modulesSource = `export const enabledModules = [{ id: 'active_module', from: '${binding}', locator: 'github:existing/provider', packageName: '${binding}' }]\n`
127
+ writeFile(path.join(appDir, 'src', 'modules.ts'), modulesSource)
128
+ const manifestBefore = fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')
129
+ const environment = installCommandStubs(appDir, binding, 'candidate_module')
130
+ const result = runMercato(['module', 'add', 'github:candidate/provider', '--allow-third-party'], appDir, environment)
131
+
132
+ expect(result.status, result.output).not.toBe(0)
133
+ expect(result.output).toContain('active_module')
134
+ expect(result.output).toContain(binding)
135
+ expect(result.output).toContain('github:existing/provider')
136
+ expect(result.output).toContain('github:candidate/provider')
137
+ expect(fs.existsSync(path.join(appDir, 'yarn-invoked'))).toBe(false)
138
+ expect(fs.readFileSync(path.join(appDir, 'src', 'modules.ts'), 'utf8')).toBe(modulesSource)
139
+ expect(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')).toBe(manifestBefore)
140
+ expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'candidate_module'))).toBe(false)
141
+ } finally {
142
+ fs.rmSync(appDir, { recursive: true, force: true })
143
+ }
144
+ })
145
+
146
+ test('rejects an npm file package runtime ID collision before invoking Yarn or copying sources', () => {
147
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-file-collision-'))
148
+ try {
149
+ createApp(appDir)
150
+ const candidateDir = path.join(appDir, 'candidate')
151
+ createPackage(candidateDir, '@publisher/candidate', ['consents'])
152
+ const modulesSource = "export const enabledModules = [{ id: 'consents', from: '@app', locator: 'github:existing/consents' }]\n"
153
+ writeFile(path.join(appDir, 'src', 'modules.ts'), modulesSource)
154
+ writeFile(path.join(appDir, 'src', 'modules', 'consents', 'index.ts'), 'export const metadata = { ejectable: true }\n')
155
+ const sourceBefore = snapshotDirectory(path.join(appDir, 'src'))
156
+ const manifestBefore = fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')
157
+ const result = runMercato(['module', 'add', '@publisher/candidate@file:./candidate', '--allow-third-party', '--eject'], appDir, installCommandStubs(appDir))
158
+
159
+ expect(result.status, result.output).not.toBe(0)
160
+ expect(result.output).toContain('consents')
161
+ expect(result.output).toMatch(/collision|already registered|conflict/i)
162
+ expect(fs.existsSync(path.join(appDir, 'yarn-invoked'))).toBe(false)
163
+ expect(snapshotDirectory(path.join(appDir, 'src'))).toEqual(sourceBefore)
164
+ expect(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')).toBe(manifestBefore)
165
+ } finally {
166
+ fs.rmSync(appDir, { recursive: true, force: true })
167
+ }
168
+ })
169
+
170
+ test('every generate command rejects duplicate effective registrations before changing existing output', () => {
171
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-generate-duplicates-'))
172
+ try {
173
+ createApp(appDir)
174
+ writeFile(path.join(appDir, 'src', 'modules.ts'), "export const enabledModules = [{ id: 'consents', from: '@app' }]\nif (process.env.COLLISION_DUPLICATE_ENABLED === 'true') enabledModules.push({ id: 'consents', from: '@app' })\n")
175
+ writeFile(path.join(appDir, 'src', 'modules', 'consents', 'index.ts'), 'export const metadata = {}\n')
176
+ const outputDir = path.join(appDir, '.mercato', 'generated')
177
+ for (const file of ['entities.ids.generated.ts', 'modules.generated.ts', 'entities.generated.ts', 'di.generated.ts', 'module-package-sources.css']) writeFile(path.join(outputDir, file), 'preserved output\n')
178
+ const before = snapshotDirectory(outputDir)
179
+
180
+ for (const command of ['all', 'entity-ids', 'registry', 'entities', 'di']) {
181
+ const result = runMercato(['generate', command, '--quiet'], appDir, { COLLISION_DUPLICATE_ENABLED: 'true' })
182
+ expect(result.status, `${command}: ${result.output}`).not.toBe(0)
183
+ expect(result.output).toContain("Duplicate enabled module ID 'consents'")
184
+ expect(snapshotDirectory(outputDir), command).toEqual(before)
185
+ }
186
+ const singleProvider = runMercato(['generate', 'all', '--quiet'], appDir, { COLLISION_DUPLICATE_ENABLED: 'false' })
187
+ expect(singleProvider.status, singleProvider.output).toBe(0)
188
+ } finally {
189
+ fs.rmSync(appDir, { recursive: true, force: true })
190
+ }
191
+ })
192
+
193
+ test('an installed alias generates binding imports, ejects with canonical provenance, and disables cleanly', () => {
194
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-alias-lifecycle-'))
195
+ try {
196
+ const binding = 'consents_candidate'
197
+ const canonicalName = '@publisher/consents'
198
+ createApp(appDir, { [binding]: `npm:${canonicalName}@0.4.7` })
199
+ const packageRoot = path.join(appDir, 'node_modules', binding)
200
+ createPackage(packageRoot, canonicalName, ['consents', 'sibling'])
201
+ writeFile(path.join(packageRoot, 'src', 'modules', 'consents', 'sibling.ts'), "export { metadata } from '../sibling'\n")
202
+ const enabled = runMercato(['module', 'enable', binding, '--module', 'consents', '--allow-third-party'], appDir)
203
+ expect(enabled.status, enabled.output).toBe(0)
204
+ const outputDir = path.join(appDir, '.mercato', 'generated')
205
+ const modulesSource = fs.readFileSync(path.join(appDir, 'src', 'modules.ts'), 'utf8')
206
+ expect(modulesSource).toContain(`from: '${binding}'`)
207
+ expect(modulesSource).toContain(`packageName: '${canonicalName}'`)
208
+ expect(fs.readFileSync(path.join(outputDir, 'di.generated.ts'), 'utf8')).toContain(`${binding}/modules/consents/di`)
209
+ expect(fs.readFileSync(path.join(outputDir, 'module-package-sources.css'), 'utf8')).toContain(`node_modules/${binding}/src/`)
210
+
211
+ const ejected = runMercato(['module', 'eject', 'consents'], appDir)
212
+ expect(ejected.status, ejected.output).toBe(0)
213
+ const ejectedSource = fs.readFileSync(path.join(appDir, 'src', 'modules.ts'), 'utf8')
214
+ expect(ejectedSource).toContain("from: '@app'")
215
+ expect(ejectedSource).toContain(`packageName: '${canonicalName}'`)
216
+ expect(ejectedSource).toContain(`locator: 'npm:${canonicalName}'`)
217
+ expect(fs.readFileSync(path.join(appDir, 'src', 'modules', 'consents', 'sibling.ts'), 'utf8')).toContain(`${binding}/modules/sibling`)
218
+
219
+ writeFile(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
220
+ const disabled = runMercato(['generate', 'all', '--quiet'], appDir)
221
+ expect(disabled.status, disabled.output).toBe(0)
222
+ expect(fs.readFileSync(path.join(outputDir, 'di.generated.ts'), 'utf8')).not.toContain('consents')
223
+ expect(fs.readFileSync(path.join(outputDir, 'module-package-sources.css'), 'utf8')).toBe('')
224
+ expect(fs.existsSync(path.join(packageRoot, 'package.json'))).toBe(true)
225
+ expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'consents', 'index.ts'))).toBe(true)
226
+ } finally {
227
+ fs.rmSync(appDir, { recursive: true, force: true })
228
+ }
229
+ })
230
+
231
+ test('missing alias exports fail enable and direct eject before source or config changes', () => {
232
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-alias-exports-'))
233
+ try {
234
+ const binding = 'consents_candidate'
235
+ createApp(appDir, { [binding]: 'npm:@publisher/consents@0.4.7' })
236
+ const packageRoot = path.join(appDir, 'node_modules', binding)
237
+ createPackage(packageRoot, '@publisher/consents', ['consents'])
238
+ writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({ name: '@publisher/consents', exports: { '.': './dist/modules/consents/index.js' } }))
239
+ const configPath = path.join(appDir, 'src', 'modules.ts')
240
+ const emptyConfig = fs.readFileSync(configPath, 'utf8')
241
+
242
+ const enabled = runMercato(['module', 'enable', binding, '--module', 'consents', '--eject', '--allow-third-party'], appDir)
243
+ expect(enabled.status, enabled.output).not.toBe(0)
244
+ expect(enabled.output).toContain(`${binding}/modules/consents/`)
245
+ expect(fs.readFileSync(configPath, 'utf8')).toBe(emptyConfig)
246
+ expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'consents'))).toBe(false)
247
+
248
+ const registeredConfig = `export const enabledModules = [{ id: 'consents', from: '${binding}' }]\n`
249
+ writeFile(configPath, registeredConfig)
250
+ const ejected = runMercato(['module', 'eject', 'consents'], appDir)
251
+ expect(ejected.status, ejected.output).not.toBe(0)
252
+ expect(ejected.output).toContain(`${binding}/modules/consents/`)
253
+ expect(fs.readFileSync(configPath, 'utf8')).toBe(registeredConfig)
254
+ expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'consents'))).toBe(false)
255
+ } finally {
256
+ fs.rmSync(appDir, { recursive: true, force: true })
257
+ }
258
+ })
259
+
260
+ test('a single package registration with a local overlay remains valid', () => {
261
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-single-overlay-'))
262
+ try {
263
+ const binding = '@publisher/consents'
264
+ createApp(appDir, { [binding]: '0.4.7' })
265
+ createPackage(path.join(appDir, 'node_modules', binding), binding, ['consents'])
266
+ writeFile(path.join(appDir, 'src', 'modules.ts'), `export const enabledModules = [{ id: 'consents', from: '${binding}' }]\n`)
267
+ writeFile(path.join(appDir, 'src', 'modules', 'consents', 'index.ts'), "export const metadata = { title: 'Local overlay' }\n")
268
+ const before = snapshotDirectory(path.join(appDir, 'src'))
269
+
270
+ const result = runMercato(['generate', 'all', '--quiet'], appDir)
271
+
272
+ expect(result.status, result.output).toBe(0)
273
+ expect(snapshotDirectory(path.join(appDir, 'src'))).toEqual(before)
274
+ expect(fs.readFileSync(path.join(appDir, '.mercato', 'generated', 'di.generated.ts'), 'utf8')).toContain(`${binding}/modules/consents/di`)
275
+ } finally {
276
+ fs.rmSync(appDir, { recursive: true, force: true })
277
+ }
278
+ })
279
+ })
@@ -9,7 +9,9 @@ import {
9
9
  listEjectableModules,
10
10
  ejectModule,
11
11
  } from '../eject'
12
- import type { PackageResolver } from '../resolver'
12
+ import { readEnabledModulesFromConfig, type PackageResolver } from '../resolver'
13
+ import { getModuleLocators } from '../module-identity'
14
+ import { resolveModuleRequirement } from '@open-mercato/shared/modules/module-locators'
13
15
 
14
16
  describe('eject', () => {
15
17
  let tmpDir: string
@@ -292,6 +294,7 @@ describe('eject', () => {
292
294
  fs.mkdirSync(path.join(installedPackageRoot, 'src', 'modules', 'test_package'), { recursive: true })
293
295
  fs.mkdirSync(path.join(installedPackageRoot, 'dist', 'modules', 'test_package'), { recursive: true })
294
296
 
297
+ fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ dependencies: { '@open-mercato/test-package': '^0.1.0' } }))
295
298
  fs.writeFileSync(
296
299
  path.join(appDir, 'src', 'modules.ts'),
297
300
  "export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n",
@@ -331,7 +334,7 @@ describe('eject', () => {
331
334
  appBase: path.join(appDir, 'src', 'modules', 'test_package'),
332
335
  pkgBase: path.join(baseDir, 'packages', 'test-package', 'src', 'modules', 'test_package'),
333
336
  }),
334
- getPackageRoot: () => path.join(baseDir, 'packages', 'test-package'),
337
+ getPackageRoot: () => installedPackageRoot,
335
338
  } as unknown as PackageResolver
336
339
  }
337
340
 
@@ -348,6 +351,95 @@ describe('eject', () => {
348
351
  ])
349
352
  })
350
353
 
354
+ it('rejects duplicate module registrations before copying source', () => {
355
+ const resolver = createOfficialModuleResolver(tmpDir)
356
+ const duplicates = [
357
+ { id: 'test_package', from: '@open-mercato/test-package' },
358
+ { id: 'test_package', from: '@other/test-package' },
359
+ ]
360
+ fs.writeFileSync(resolver.getModulesConfigPath(), `export const enabledModules = ${JSON.stringify(duplicates)}`)
361
+ const sourceBefore = fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')
362
+ expect(() => ejectModule({ ...resolver, loadEnabledModules: () => duplicates }, 'test_package')).toThrow("Duplicate enabled module ID 'test_package'")
363
+ expect(fs.existsSync(resolver.getModulePaths(duplicates[0]).appBase)).toBe(false)
364
+ expect(fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')).toBe(sourceBefore)
365
+ })
366
+
367
+ it('rejects duplicate registrations elsewhere in the effective set before copying', () => {
368
+ const resolver = createOfficialModuleResolver(tmpDir)
369
+ const entries = [...resolver.loadEnabledModules(), { id: 'customers' }, { id: 'customers', from: '@other/customers' }]
370
+ const before = fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')
371
+
372
+ expect(() => ejectModule({ ...resolver, loadEnabledModules: () => entries }, 'test_package')).toThrow("Duplicate enabled module ID 'customers'")
373
+ expect(fs.existsSync(resolver.getModulePaths(entries[0]).appBase)).toBe(false)
374
+ expect(fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')).toBe(before)
375
+ })
376
+
377
+ it('checks the config mutation before copying when effective entries cannot be edited', () => {
378
+ const resolver = createOfficialModuleResolver(tmpDir)
379
+ const before = "const entries = [{ id: 'test_package', from: '@open-mercato/test-package' }]; export const enabledModules = entries;\n"
380
+ fs.writeFileSync(resolver.getModulesConfigPath(), before)
381
+
382
+ expect(() => ejectModule(resolver, 'test_package')).toThrow('Could not find enabledModules array')
383
+ expect(fs.existsSync(resolver.getModulePaths(resolver.loadEnabledModules()[0]).appBase)).toBe(false)
384
+ expect(fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')).toBe(before)
385
+ })
386
+
387
+ it('rejects contradictory observed provenance before copying source', () => {
388
+ const resolver = createOfficialModuleResolver(tmpDir)
389
+ const entry = { id: 'test_package', from: '@open-mercato/test-package', locator: 'github:unexpected/provider' }
390
+ const before = "export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package', locator: 'github:unexpected/provider' }]\n"
391
+ fs.writeFileSync(resolver.getModulesConfigPath(), before)
392
+
393
+ expect(() => ejectModule({ ...resolver, loadEnabledModules: () => [entry] }, 'test_package')).toThrow('disagrees with observed origin')
394
+ expect(fs.existsSync(resolver.getModulePaths(entry).appBase)).toBe(false)
395
+ expect(fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')).toBe(before)
396
+ })
397
+
398
+ it('preserves an existing local overlay and gives a recovery without deleting source', () => {
399
+ const resolver = createOfficialModuleResolver(tmpDir)
400
+ const appBase = resolver.getModulePaths(resolver.loadEnabledModules()[0]).appBase
401
+ fs.mkdirSync(appBase, { recursive: true })
402
+ fs.writeFileSync(path.join(appBase, 'local.ts'), 'export const local = true\n')
403
+ const before = fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')
404
+
405
+ expect(() => ejectModule(resolver, 'test_package')).toThrow('Keep the existing local source')
406
+ expect(fs.readFileSync(path.join(appBase, 'local.ts'), 'utf8')).toBe('export const local = true\n')
407
+ expect(fs.existsSync(path.join(appBase, 'index.ts'))).toBe(false)
408
+ expect(fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')).toBe(before)
409
+ })
410
+
411
+ it('keeps canonical provenance and rewrites sibling imports using the installed alias', () => {
412
+ const resolver = createOfficialModuleResolver(tmpDir)
413
+ const packageRoot = resolver.getPackageRoot('@open-mercato/test-package')
414
+ const aliasRoot = path.join(resolver.getAppDir(), 'node_modules', 'candidate_consents')
415
+ fs.mkdirSync(path.dirname(aliasRoot), { recursive: true })
416
+ fs.symlinkSync(packageRoot, aliasRoot, 'dir')
417
+ fs.writeFileSync(path.join(resolver.getAppDir(), 'package.json'), JSON.stringify({ dependencies: { candidate_consents: 'npm:@open-mercato/test-package@1.0.0' } }))
418
+ const entry = { id: 'test_package', from: 'candidate_consents' }
419
+ fs.writeFileSync(resolver.getModulesConfigPath(), `export const enabledModules = [${JSON.stringify(entry)}]\n`)
420
+ fs.mkdirSync(path.join(packageRoot, 'src', 'modules', 'sibling'), { recursive: true })
421
+ fs.writeFileSync(path.join(packageRoot, 'src', 'modules', 'sibling', 'index.ts'), 'export const sibling = true\n')
422
+ fs.writeFileSync(path.join(packageRoot, 'src', 'modules', 'test_package', 'widget.ts'), "export { sibling } from '../sibling'\n")
423
+ const aliasResolver = { ...resolver, loadEnabledModules: () => [entry], getPackageRoot: () => aliasRoot }
424
+
425
+ ejectModule(aliasResolver, 'test_package')
426
+
427
+ expect(readEnabledModulesFromConfig(resolver.getModulesConfigPath())[0]).toEqual({ id: 'test_package', from: '@app', packageName: '@open-mercato/test-package', locator: 'npm:@open-mercato/test-package' })
428
+ const appBase = resolver.getModulePaths(entry).appBase
429
+ expect(fs.readFileSync(path.join(appBase, 'widget.ts'), 'utf8')).toContain("from 'candidate_consents/modules/sibling'")
430
+ })
431
+
432
+ it('preserves GitHub origin and canonical package attribution through legacy eject', () => {
433
+ const resolver = createOfficialModuleResolver(tmpDir)
434
+ fs.writeFileSync(path.join(resolver.getAppDir(), 'package.json'), JSON.stringify({ dependencies: { '@open-mercato/test-package': 'github:pkarw/test-package#main' } }))
435
+ ejectModule(resolver, 'test_package')
436
+ const entry = readEnabledModulesFromConfig(resolver.getModulesConfigPath())[0]
437
+ expect(entry).toEqual({ id: 'test_package', from: '@app', packageName: '@open-mercato/test-package', locator: 'github:pkarw/test-package' })
438
+ const identities = [{ id: entry.id, locators: getModuleLocators(entry, resolver) }]
439
+ expect(resolveModuleRequirement('npm:@open-mercato/test-package', identities)).toBeUndefined()
440
+ expect(resolveModuleRequirement('github:pkarw/test-package', identities)).toBe('test_package')
441
+ })
442
+
351
443
  it('ejects module packages using module index.ts ejectable flag', () => {
352
444
  const resolver = createOfficialModuleResolver(tmpDir)
353
445
  const appModuleDir = path.join(tmpDir, 'apps', 'mercato', 'src', 'modules', 'test_package')
@@ -358,7 +450,7 @@ describe('eject', () => {
358
450
  expect(fs.existsSync(path.join(appModuleDir, 'index.ts'))).toBe(true)
359
451
  expect(fs.existsSync(path.join(appModuleDir, 'widget.ts'))).toBe(true)
360
452
  expect(fs.readFileSync(modulesTsPath, 'utf8')).toContain(
361
- "{ id: 'test_package', from: '@app' }",
453
+ "{ id: 'test_package', from: '@app', locator: 'npm:@open-mercato/test-package', packageName: '@open-mercato/test-package' }",
362
454
  )
363
455
  })
364
456
  })
@@ -381,6 +473,8 @@ describe('eject', () => {
381
473
  path.join(appDir, 'src', 'modules.ts'),
382
474
  "export const enabledModules = [{ id: 'customers', from: '@open-mercato/core' }]\n",
383
475
  )
476
+ fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ name: '@open-mercato/core' }))
477
+ fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ dependencies: { '@open-mercato/core': '^0.1.0' } }))
384
478
  fs.writeFileSync(
385
479
  path.join(distModuleDir, 'index.js'),
386
480
  'var metadata = { title: "Customers", description: "Compiled", ejectable: true };\nexport { metadata };\n',
@@ -431,7 +525,7 @@ describe('eject', () => {
431
525
  expect(fs.existsSync(path.join(appModuleDir, 'index.ts'))).toBe(true)
432
526
  expect(fs.existsSync(path.join(appModuleDir, 'acl.ts'))).toBe(true)
433
527
  expect(fs.existsSync(path.join(appModuleDir, 'index.js'))).toBe(false)
434
- expect(fs.readFileSync(modulesTsPath, 'utf8')).toContain("{ id: 'customers', from: '@app' }")
528
+ expect(fs.readFileSync(modulesTsPath, 'utf8')).toContain("{ id: 'customers', from: '@app', locator: 'npm:@open-mercato/core', packageName: '@open-mercato/core' }")
435
529
  })
436
530
 
437
531
  it('falls back to compiled index.js metadata for dist-only packages', () => {
@@ -1,4 +1,6 @@
1
- import { resolveInitDerivedSecrets } from '../init-secrets'
1
+ import { readFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { resolveInitDerivedSecrets, resolveInitSuperadminCredentials } from '../init-secrets'
2
4
 
3
5
  describe('resolveInitDerivedSecrets', () => {
4
6
  it("seeds the well-known 'secret' password in non-production when no env overrides are set", () => {
@@ -77,3 +79,85 @@ describe('resolveInitDerivedSecrets', () => {
77
79
  })
78
80
  })
79
81
 
82
+
83
+ describe('production bootstrap superadmin credentials', () => {
84
+ const production = { NODE_ENV: 'production' }
85
+
86
+ it.each([undefined, '', ' ', 'secret', 'password', ' PASSWORD ', 'changeme', 'change-me', 'replace_me'])(
87
+ 'rejects an absent or placeholder production password: %s', (password) => {
88
+ expect(() => resolveInitSuperadminCredentials([], {
89
+ ...production, OM_INIT_SUPERADMIN_PASSWORD: password,
90
+ })).toThrow('requires a unique superadmin password')
91
+ },
92
+ )
93
+
94
+ it.each(['--skip-password-policy', '--skip-password-policy=true', '--allow-weak-password', '--allow-weak-password=true'])(
95
+ 'enforces production policy despite %s', (flag) => {
96
+ expect(() => resolveInitSuperadminCredentials([flag, '--password=weak'], production))
97
+ .toThrow('does not meet the configured password policy')
98
+ expect(resolveInitSuperadminCredentials([flag, '--password=Unique-Owner-937!'], production).skipPasswordPolicy).toBe(false)
99
+ },
100
+ )
101
+
102
+ it('uses the configured policy and never includes the password in errors', () => {
103
+ const password = 'Unique-Owner-937!'
104
+ expect(() => resolveInitSuperadminCredentials([`--password=${password}`], {
105
+ ...production, OM_PASSWORD_MIN_LENGTH: '40',
106
+ })).toThrow('does not meet the configured password policy')
107
+ try {
108
+ resolveInitSuperadminCredentials([`--password=${password}`], { ...production, OM_PASSWORD_MIN_LENGTH: '40' })
109
+ } catch (error) {
110
+ expect(String(error)).not.toContain(password)
111
+ }
112
+ })
113
+
114
+ it('rejects placeholders even with a relaxed policy', () => {
115
+ expect(() => resolveInitSuperadminCredentials([], {
116
+ ...production, OM_INIT_SUPERADMIN_PASSWORD: 'password', OM_PASSWORD_REQUIRE_DIGIT: 'false',
117
+ OM_PASSWORD_REQUIRE_UPPERCASE: 'false', OM_PASSWORD_REQUIRE_SPECIAL: 'false',
118
+ })).toThrow('requires a unique superadmin password')
119
+ })
120
+
121
+ it('prefers command arguments over environment overrides', () => {
122
+ expect(resolveInitSuperadminCredentials(['--email=owner@example.test', '--password=Unique-Owner-937!'], {
123
+ NODE_ENV: ' Production ', OM_INIT_SUPERADMIN_PASSWORD: 'password', OM_INIT_SUPERADMIN_EMAIL: 'other@example.test',
124
+ })).toEqual({ email: 'owner@example.test', password: 'Unique-Owner-937!', skipPasswordPolicy: false })
125
+ })
126
+
127
+ it('uses the CLI locale for operator-facing production diagnostics', () => {
128
+ expect(() => resolveInitSuperadminCredentials([], { ...production, LC_ALL: 'pl_PL.UTF-8' }))
129
+ .toThrow('Inicjalizacja produkcyjna wymaga')
130
+ expect(() => resolveInitSuperadminCredentials([], { ...production, LC_ALL: 'unknown' }))
131
+ .toThrow('Production initialization requires')
132
+ })
133
+
134
+ it('accepts a policy-compliant environment password', () => {
135
+ expect(resolveInitSuperadminCredentials([], {
136
+ ...production, OM_INIT_SUPERADMIN_PASSWORD: 'Unique-Owner-937!',
137
+ }).password).toBe('Unique-Owner-937!')
138
+ })
139
+
140
+ it.each([undefined, 'development', 'test', 'staging'])('preserves non-production defaults for %s', (nodeEnv) => {
141
+ expect(resolveInitSuperadminCredentials([], { NODE_ENV: nodeEnv })).toEqual({
142
+ email: 'superadmin@acme.com', password: 'secret', skipPasswordPolicy: true,
143
+ })
144
+ expect(resolveInitSuperadminCredentials([], { NODE_ENV: nodeEnv, OM_INIT_SUPERADMIN_PASSWORD: 'password' }).password).toBe('password')
145
+ expect(resolveInitSuperadminCredentials(['--skip-password-policy=false'], { NODE_ENV: nodeEnv }).skipPasswordPolicy).toBe(false)
146
+ })
147
+ })
148
+
149
+ describe('full-app Compose bootstrap defaults', () => {
150
+ it.each(['starters/docker/compose.fullapp.yml', 'packages/create-app/template/docker-compose.fullapp.yml'])(
151
+ '%s cannot bootstrap predictable production credentials', (composePath) => {
152
+ const compose = readFileSync(path.resolve(__dirname, '../../../../..', composePath), 'utf8')
153
+ const fallback = compose.match(/OM_INIT_SUPERADMIN_PASSWORD: \$\{OM_INIT_SUPERADMIN_PASSWORD:-(.*?)\}/)?.[1]
154
+ expect(fallback).toBeDefined()
155
+ expect(() => resolveInitSuperadminCredentials([], {
156
+ NODE_ENV: 'production', OM_INIT_SUPERADMIN_PASSWORD: fallback,
157
+ })).toThrow('requires a unique superadmin password')
158
+ expect(resolveInitSuperadminCredentials([], {
159
+ NODE_ENV: 'development', OM_INIT_SUPERADMIN_PASSWORD: fallback,
160
+ }).password).toBe(fallback)
161
+ },
162
+ )
163
+ })
@@ -0,0 +1,50 @@
1
+ import { formatInitSummary } from '../init-summary'
2
+
3
+ const users = [
4
+ { icon: '👑', label: 'Superadmin', email: 'owner@example.test', password: 'demo-superadmin' },
5
+ { icon: '🧰', label: 'Admin', email: 'admin@example.test', password: 'demo-admin' },
6
+ { icon: '👷', label: 'Employee', email: 'employee@example.test', password: 'demo-employee' },
7
+ ]
8
+
9
+ describe('formatInitSummary', () => {
10
+ it('renders every supplied account and its own password without side borders', () => {
11
+ const output = formatInitSummary({ users, autoStart: true }).join('\n')
12
+ for (const user of users) {
13
+ expect(output).toContain(`${user.icon} ${user.label}`)
14
+ expect(output).toContain(user.email)
15
+ expect(output).toContain(user.password)
16
+ }
17
+ expect(output).not.toMatch(/[╔║╚]/u)
18
+ expect(output).toContain('The dev server will start automatically.')
19
+ expect(output).not.toContain('\u001b[')
20
+ })
21
+
22
+ it('shows the manual startup hint when initialization is invoked directly', () => {
23
+ const output = formatInitSummary({ users: users.slice(0, 1), autoStart: false }).join('\n')
24
+ expect(output).toContain('🚀 Start the dev server: yarn dev')
25
+ expect(output).not.toContain('automatically')
26
+ expect(output).not.toContain('🧰 Admin')
27
+ })
28
+
29
+ it.each([8, 20, 40, 80])('wraps long credentials within %i columns without dropping characters', (columns) => {
30
+ const password = 'long-demo-password-'.repeat(12)
31
+ const lines = formatInitSummary({ users: [{ ...users[0], password }], autoStart: true, columns })
32
+ for (const line of lines) {
33
+ const width = [...line].reduce((total, character) => total + (/\p{Extended_Pictographic}/u.test(character) ? 2 : 1), 0)
34
+ expect(width).toBeLessThanOrEqual(columns)
35
+ }
36
+ expect(lines.map((line) => line.trim()).join('')).toContain(password)
37
+ })
38
+
39
+ it('paints complete rows dark gray with light text and resets after each row', () => {
40
+ const lines = formatInitSummary({ users, autoStart: true, color: true, columns: 80 })
41
+ for (const line of lines) {
42
+ expect(line).toMatch(/^\u001b\[48;2;57;62;70m\u001b\[38;2;238;238;238m/u)
43
+ expect(line).toMatch(/\u001b\[0m$/u)
44
+ }
45
+ })
46
+
47
+ it.each([NaN, Infinity, 0, -1])('uses a safe width for invalid columns %s', (columns) => {
48
+ expect(formatInitSummary({ users, autoStart: false, columns })[0]).toHaveLength(80)
49
+ })
50
+ })