backend-manager 5.2.18 → 5.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Test: mergeLineBasedFiles()
3
+ * Unit tests for the line-based .env / .gitignore / CLAUDE.md merge.
4
+ *
5
+ * Tests the pure function directly — no emulator, no Firestore, no HTTP.
6
+ *
7
+ * Regression coverage for the bug that scrambled consumers' `.env` files: an
8
+ * older positional implementation zipped comment lines and value lines by index,
9
+ * so any drift in key order shifted every value under the wrong header. The
10
+ * canonical impl merges BY KEY, so headers always re-anchor to the template and
11
+ * values follow their key across sections.
12
+ */
13
+ const {
14
+ mergeLineBasedFiles,
15
+ DEFAULT_MARKER,
16
+ CUSTOM_MARKER,
17
+ } = require('../../src/utils/merge-line-files.js');
18
+
19
+ // Setup-test helper shim — must re-export the SAME canonical impl (SSOT).
20
+ const helperShim = require('../../src/cli/commands/setup-tests/helpers/merge-line-files.js');
21
+
22
+ // A representative framework .env template (keys grouped under headers).
23
+ const TEMPLATE = [
24
+ DEFAULT_MARKER,
25
+ '# GitHub',
26
+ 'GH_TOKEN=""',
27
+ '',
28
+ '# AI',
29
+ 'OPENAI_API_KEY=""',
30
+ 'ANTHROPIC_API_KEY=""',
31
+ '',
32
+ '# Payment Processors',
33
+ 'PAYPAL_CLIENT_SECRET=""',
34
+ 'STRIPE_SECRET_KEY=""',
35
+ '',
36
+ CUSTOM_MARKER,
37
+ '# Add your custom environment variables below this line',
38
+ ].join('\n');
39
+
40
+ // Pull the value assigned to a key in a given section of a merged result.
41
+ function keyValueInSection(merged, key, section /* 'default' | 'custom' */) {
42
+ const lines = merged.split('\n');
43
+ let mode = null;
44
+ for (const line of lines) {
45
+ const trimmed = line.trim();
46
+ if (trimmed === DEFAULT_MARKER) { mode = 'default'; continue; }
47
+ if (trimmed === CUSTOM_MARKER) { mode = 'custom'; continue; }
48
+ if (mode !== section || !trimmed || trimmed.startsWith('#')) continue;
49
+ const eq = trimmed.indexOf('=');
50
+ if (eq < 0) continue;
51
+ if (trimmed.slice(0, eq).trim() === key) return trimmed.slice(eq + 1);
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ // Count how many times a key appears anywhere (catches duplication bugs).
57
+ function keyOccurrences(merged, key) {
58
+ return merged.split('\n').filter((l) => l.trim().startsWith(`${key}=`)).length;
59
+ }
60
+
61
+ // Assert a key sits directly under the expected comment header in the merged output.
62
+ function headerAboveKey(merged, key) {
63
+ const lines = merged.split('\n');
64
+ const idx = lines.findIndex((l) => l.trim().startsWith(`${key}=`));
65
+ if (idx < 0) return null;
66
+ for (let i = idx - 1; i >= 0; i--) {
67
+ const t = lines[i].trim();
68
+ if (t.startsWith('#') && t !== DEFAULT_MARKER && t !== CUSTOM_MARKER) return t;
69
+ if (t && !t.startsWith('#')) continue; // another key — keep scanning up
70
+ }
71
+ return null;
72
+ }
73
+
74
+ module.exports = {
75
+ description: 'mergeLineBasedFiles() line-based .env / .gitignore merge',
76
+ type: 'group',
77
+
78
+ tests: [
79
+ {
80
+ name: 'ssot-helper-shim-is-same-function',
81
+ async run({ assert }) {
82
+ assert.equal(
83
+ helperShim.mergeLineBasedFiles,
84
+ mergeLineBasedFiles,
85
+ 'setup-tests helper must re-export the canonical mergeLineBasedFiles (SSOT)'
86
+ );
87
+ assert.equal(helperShim.DEFAULT_SECTION_MARKER, DEFAULT_MARKER, 'marker alias matches');
88
+ assert.equal(helperShim.CUSTOM_SECTION_MARKER, CUSTOM_MARKER, 'marker alias matches');
89
+ assert.ok(helperShim.hasSectionMarkers(TEMPLATE), 'hasSectionMarkers detects markers');
90
+ },
91
+ },
92
+
93
+ {
94
+ name: 'preserves-existing-values-by-key',
95
+ async run({ assert }) {
96
+ const existing = [
97
+ DEFAULT_MARKER,
98
+ '# GitHub',
99
+ 'GH_TOKEN="ghp_real"',
100
+ '# AI',
101
+ 'OPENAI_API_KEY="sk-real"',
102
+ 'ANTHROPIC_API_KEY=""',
103
+ '# Payment Processors',
104
+ 'PAYPAL_CLIENT_SECRET="pp_real"',
105
+ 'STRIPE_SECRET_KEY=""',
106
+ CUSTOM_MARKER,
107
+ ].join('\n');
108
+
109
+ const merged = mergeLineBasedFiles(existing, TEMPLATE, '.env');
110
+ assert.equal(keyValueInSection(merged, 'GH_TOKEN', 'default'), '"ghp_real"', 'GH_TOKEN value kept');
111
+ assert.equal(keyValueInSection(merged, 'OPENAI_API_KEY', 'default'), '"sk-real"', 'OPENAI value kept');
112
+ assert.equal(keyValueInSection(merged, 'PAYPAL_CLIENT_SECRET', 'default'), '"pp_real"', 'PAYPAL value kept');
113
+ },
114
+ },
115
+
116
+ {
117
+ name: 'keys-stay-under-correct-header-when-order-drifts',
118
+ async run({ assert }) {
119
+ // Existing file has keys in a DIFFERENT order than the template. The old
120
+ // positional merge would shift values under the wrong header here.
121
+ const existing = [
122
+ DEFAULT_MARKER,
123
+ '# Misc (old grouping)',
124
+ 'STRIPE_SECRET_KEY="stripe_real"',
125
+ 'OPENAI_API_KEY="openai_real"',
126
+ 'GH_TOKEN="gh_real"',
127
+ 'PAYPAL_CLIENT_SECRET="pp_real"',
128
+ 'ANTHROPIC_API_KEY="anthropic_real"',
129
+ CUSTOM_MARKER,
130
+ ].join('\n');
131
+
132
+ const merged = mergeLineBasedFiles(existing, TEMPLATE, '.env');
133
+
134
+ assert.equal(headerAboveKey(merged, 'OPENAI_API_KEY'), '# AI', 'OPENAI under # AI');
135
+ assert.equal(headerAboveKey(merged, 'ANTHROPIC_API_KEY'), '# AI', 'ANTHROPIC under # AI');
136
+ assert.equal(headerAboveKey(merged, 'PAYPAL_CLIENT_SECRET'), '# Payment Processors', 'PAYPAL under # Payment Processors');
137
+ assert.equal(headerAboveKey(merged, 'STRIPE_SECRET_KEY'), '# Payment Processors', 'STRIPE under # Payment Processors');
138
+ assert.equal(headerAboveKey(merged, 'GH_TOKEN'), '# GitHub', 'GH_TOKEN under # GitHub');
139
+
140
+ // Values still attached to the right keys.
141
+ assert.equal(keyValueInSection(merged, 'STRIPE_SECRET_KEY', 'default'), '"stripe_real"', 'STRIPE value intact');
142
+ assert.equal(keyValueInSection(merged, 'OPENAI_API_KEY', 'default'), '"openai_real"', 'OPENAI value intact');
143
+ },
144
+ },
145
+
146
+ {
147
+ name: 'promotes-custom-key-to-default-when-template-adopts-it',
148
+ async run({ assert }) {
149
+ // User had APOLLO_API_KEY in Custom; framework adds it to the template.
150
+ const tplWithApollo = TEMPLATE.replace('STRIPE_SECRET_KEY=""', 'STRIPE_SECRET_KEY=""\nAPOLLO_API_KEY=""');
151
+ const existing = [
152
+ DEFAULT_MARKER,
153
+ '# GitHub',
154
+ 'GH_TOKEN="gh_real"',
155
+ '# AI',
156
+ 'OPENAI_API_KEY=""',
157
+ 'ANTHROPIC_API_KEY=""',
158
+ '# Payment Processors',
159
+ 'PAYPAL_CLIENT_SECRET=""',
160
+ 'STRIPE_SECRET_KEY=""',
161
+ CUSTOM_MARKER,
162
+ 'APOLLO_API_KEY="apollo_real"',
163
+ 'GITHUB_TOKEN="legacy"',
164
+ ].join('\n');
165
+
166
+ const merged = mergeLineBasedFiles(existing, tplWithApollo, '.env');
167
+
168
+ assert.equal(keyValueInSection(merged, 'APOLLO_API_KEY', 'default'), '"apollo_real"', 'APOLLO promoted to Default with value');
169
+ assert.equal(keyValueInSection(merged, 'APOLLO_API_KEY', 'custom'), undefined, 'APOLLO removed from Custom');
170
+ assert.equal(keyOccurrences(merged, 'APOLLO_API_KEY'), 1, 'APOLLO not duplicated');
171
+ },
172
+ },
173
+
174
+ {
175
+ name: 'migrates-unknown-default-key-to-custom',
176
+ async run({ assert }) {
177
+ // User has a legacy key in Default that the template no longer defines.
178
+ const existing = [
179
+ DEFAULT_MARKER,
180
+ '# GitHub',
181
+ 'GITHUB_TOKEN="legacy_real"', // template now uses GH_TOKEN
182
+ 'GH_TOKEN="gh_real"',
183
+ '# AI',
184
+ 'OPENAI_API_KEY=""',
185
+ 'ANTHROPIC_API_KEY=""',
186
+ '# Payment Processors',
187
+ 'PAYPAL_CLIENT_SECRET=""',
188
+ 'STRIPE_SECRET_KEY=""',
189
+ CUSTOM_MARKER,
190
+ ].join('\n');
191
+
192
+ const merged = mergeLineBasedFiles(existing, TEMPLATE, '.env');
193
+
194
+ assert.equal(keyValueInSection(merged, 'GITHUB_TOKEN', 'custom'), '"legacy_real"', 'legacy key migrated to Custom with value');
195
+ assert.equal(keyValueInSection(merged, 'GITHUB_TOKEN', 'default'), undefined, 'legacy key gone from Default');
196
+ assert.equal(keyValueInSection(merged, 'GH_TOKEN', 'default'), '"gh_real"', 'GH_TOKEN kept in Default');
197
+ },
198
+ },
199
+
200
+ {
201
+ name: 'preserves-custom-section-verbatim',
202
+ async run({ assert }) {
203
+ const existing = [
204
+ DEFAULT_MARKER,
205
+ '# GitHub',
206
+ 'GH_TOKEN=""',
207
+ '# AI',
208
+ 'OPENAI_API_KEY=""',
209
+ 'ANTHROPIC_API_KEY=""',
210
+ '# Payment Processors',
211
+ 'PAYPAL_CLIENT_SECRET=""',
212
+ 'STRIPE_SECRET_KEY=""',
213
+ CUSTOM_MARKER,
214
+ 'MY_CUSTOM_KEY="custom_real"',
215
+ '# OAuth2',
216
+ 'OAUTH_ID="abc"',
217
+ ].join('\n');
218
+
219
+ const merged = mergeLineBasedFiles(existing, TEMPLATE, '.env');
220
+ assert.equal(keyValueInSection(merged, 'MY_CUSTOM_KEY', 'custom'), '"custom_real"', 'custom key preserved');
221
+ assert.equal(keyValueInSection(merged, 'OAUTH_ID', 'custom'), '"abc"', 'custom OAuth key preserved');
222
+ assert.ok(merged.includes('# OAuth2'), 'custom comment preserved');
223
+ },
224
+ },
225
+
226
+ {
227
+ name: 'normalizes-raw-values-to-double-quoted',
228
+ async run({ assert }) {
229
+ const existing = [
230
+ DEFAULT_MARKER,
231
+ '# GitHub',
232
+ 'GH_TOKEN=raw-no-quotes',
233
+ '# AI',
234
+ "OPENAI_API_KEY='single'",
235
+ 'ANTHROPIC_API_KEY=""',
236
+ '# Payment Processors',
237
+ 'PAYPAL_CLIENT_SECRET=""',
238
+ 'STRIPE_SECRET_KEY=""',
239
+ CUSTOM_MARKER,
240
+ ].join('\n');
241
+
242
+ const merged = mergeLineBasedFiles(existing, TEMPLATE, '.env');
243
+ assert.equal(keyValueInSection(merged, 'GH_TOKEN', 'default'), '"raw-no-quotes"', 'raw value double-quoted');
244
+ assert.equal(keyValueInSection(merged, 'OPENAI_API_KEY', 'default'), '"single"', 'single-quoted canonicalized to double');
245
+ },
246
+ },
247
+
248
+ {
249
+ name: 'is-idempotent',
250
+ async run({ assert }) {
251
+ const existing = [
252
+ DEFAULT_MARKER,
253
+ '# GitHub',
254
+ 'GH_TOKEN="gh_real"',
255
+ '# AI',
256
+ 'OPENAI_API_KEY="sk"',
257
+ 'ANTHROPIC_API_KEY=""',
258
+ '# Payment Processors',
259
+ 'PAYPAL_CLIENT_SECRET=""',
260
+ 'STRIPE_SECRET_KEY=""',
261
+ CUSTOM_MARKER,
262
+ 'CUSTOM="x"',
263
+ ].join('\n');
264
+
265
+ const once = mergeLineBasedFiles(existing, TEMPLATE, '.env');
266
+ const twice = mergeLineBasedFiles(once, TEMPLATE, '.env');
267
+ assert.equal(once, twice, 're-running the merge produces identical output');
268
+ },
269
+ },
270
+ ],
271
+ };