@mettlecast/domain-cli 0.2.59 → 0.2.60
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/builder/build-registry.js +128 -13
- package/dist/builder/load-module.js +3 -3
- package/dist/cli.js +2 -2
- package/dist/commands/build-catalog.d.ts +2 -2
- package/dist/commands/build-catalog.js +5 -5
- package/dist/commands/build-flows.js +1 -1
- package/dist/commands/build.js +5 -5
- package/dist/commands/check-hashes.js +2 -2
- package/dist/commands/create-project.js +1 -1
- package/dist/commands/doctor.d.ts +5 -7
- package/dist/commands/doctor.js +44 -165
- package/dist/commands/power-tune.js +2 -2
- package/dist/commands/show-dns.d.ts +1 -1
- package/dist/commands/show-dns.js +5 -5
- package/dist/commands/upgrade.js +17 -11
- package/dist/commands/validate.js +183 -0
- package/dist/utils/header-inject.js +2 -2
- package/dist/utils/install-file.d.ts +1 -1
- package/dist/utils/install-file.js +1 -1
- package/dist/utils/manifest.js +1 -2
- package/dist/utils/scaffold-config.d.ts +2 -2
- package/dist/utils/scaffold-config.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/commands/check-hashes.test.ts +9 -9
- package/src/__tests__/commands/upgrade.test.ts +7 -7
- package/src/__tests__/doctor.test.ts +60 -67
- package/src/__tests__/package-freshness.test.ts +114 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
- package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
- package/src/__tests__/utils/install-file.test.ts +2 -2
- package/src/__tests__/utils/manifest.test.ts +2 -2
- package/src/__tests__/validate.test.ts +652 -1
- package/src/builder/build-registry.ts +147 -15
- package/src/builder/load-module.ts +3 -3
- package/src/cli.ts +3 -3
- package/src/commands/build-catalog.ts +5 -5
- package/src/commands/build-flows.ts +1 -1
- package/src/commands/build.ts +5 -5
- package/src/commands/check-hashes.ts +2 -2
- package/src/commands/create-project.ts +1 -1
- package/src/commands/doctor.ts +52 -181
- package/src/commands/power-tune.ts +2 -2
- package/src/commands/show-dns.ts +5 -5
- package/src/commands/upgrade.ts +16 -10
- package/src/commands/validate.ts +226 -1
- package/src/utils/header-inject.ts +2 -2
- package/src/utils/install-file.ts +1 -1
- package/src/utils/manifest.ts +1 -2
- package/src/utils/scaffold-config.ts +3 -3
|
@@ -21,12 +21,12 @@ describe('runDoctor', () => {
|
|
|
21
21
|
|
|
22
22
|
it('returns a DoctorReport with checks and exitCode', async () => {
|
|
23
23
|
// Create minimal project structure
|
|
24
|
-
await mkdir(join(tempDir, '.
|
|
24
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
25
25
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
26
26
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
27
27
|
|
|
28
28
|
await writeFile(
|
|
29
|
-
join(tempDir, '.
|
|
29
|
+
join(tempDir, '.mc', 'scaffold-config.json'),
|
|
30
30
|
JSON.stringify({ domainIds: [] })
|
|
31
31
|
);
|
|
32
32
|
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh\necho "hook"');
|
|
@@ -42,9 +42,9 @@ describe('runDoctor', () => {
|
|
|
42
42
|
|
|
43
43
|
it('reports FAIL when domain config is missing', async () => {
|
|
44
44
|
await mkdir(join(tempDir, 'domains', 'test-domain'), { recursive: true });
|
|
45
|
-
await mkdir(join(tempDir, '.
|
|
45
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
46
46
|
await writeFile(
|
|
47
|
-
join(tempDir, '.
|
|
47
|
+
join(tempDir, '.mc', 'scaffold-config.json'),
|
|
48
48
|
JSON.stringify({ domainIds: ['test-domain'] })
|
|
49
49
|
);
|
|
50
50
|
|
|
@@ -56,9 +56,9 @@ describe('runDoctor', () => {
|
|
|
56
56
|
|
|
57
57
|
it('exits with code 1 when FAIL present (always strict)', async () => {
|
|
58
58
|
await mkdir(join(tempDir, 'domains', 'bad-domain'), { recursive: true });
|
|
59
|
-
await mkdir(join(tempDir, '.
|
|
59
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
60
60
|
await writeFile(
|
|
61
|
-
join(tempDir, '.
|
|
61
|
+
join(tempDir, '.mc', 'scaffold-config.json'),
|
|
62
62
|
JSON.stringify({ domainIds: ['bad-domain'] })
|
|
63
63
|
);
|
|
64
64
|
|
|
@@ -68,11 +68,11 @@ describe('runDoctor', () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
it('reports PASS for Husky when hook exists', async () => {
|
|
71
|
-
await mkdir(join(tempDir, '.
|
|
71
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
72
72
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
73
73
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
74
74
|
await writeFile(
|
|
75
|
-
join(tempDir, '.
|
|
75
|
+
join(tempDir, '.mc', 'scaffold-config.json'),
|
|
76
76
|
JSON.stringify({ domainIds: [] })
|
|
77
77
|
);
|
|
78
78
|
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
@@ -84,11 +84,11 @@ describe('runDoctor', () => {
|
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
it('includes all 15 checks in the report', async () => {
|
|
87
|
-
await mkdir(join(tempDir, '.
|
|
87
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
88
88
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
89
89
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
90
90
|
await writeFile(
|
|
91
|
-
join(tempDir, '.
|
|
91
|
+
join(tempDir, '.mc', 'scaffold-config.json'),
|
|
92
92
|
JSON.stringify({ domainIds: [] })
|
|
93
93
|
);
|
|
94
94
|
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
@@ -112,16 +112,16 @@ describe('runDoctor', () => {
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
describe('layout validation checks', () => {
|
|
115
|
-
it('PASS when manifest has
|
|
116
|
-
await mkdir(join(tempDir, '.
|
|
115
|
+
it('PASS when manifest has managed files in current installed locations', async () => {
|
|
116
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
117
117
|
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
118
118
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
119
119
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
120
|
-
await mkdir(join(tempDir, '
|
|
120
|
+
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
121
121
|
|
|
122
|
-
await writeFile(join(tempDir, '
|
|
122
|
+
await writeFile(join(tempDir, 'infra', 'modules', 'app.ts'), '// @mc-scaffold: domain@1.0.0\n');
|
|
123
123
|
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
124
|
-
await writeFile(join(tempDir, '.
|
|
124
|
+
await writeFile(join(tempDir, '.mc', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
125
125
|
|
|
126
126
|
const manifest = {
|
|
127
127
|
$schema: 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json',
|
|
@@ -132,19 +132,19 @@ describe('runDoctor', () => {
|
|
|
132
132
|
awsRegion: 'eu-north-1',
|
|
133
133
|
enabledModules: ['domain'],
|
|
134
134
|
files: [
|
|
135
|
-
{ path: '
|
|
135
|
+
{ path: 'infra/modules/app.ts', module: 'domain', moduleVersion: '1.0.0',
|
|
136
136
|
sha256: 'abc', wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'managed' as const }
|
|
137
137
|
]
|
|
138
138
|
};
|
|
139
139
|
await writeFile(join(tempDir, '.mc', 'manifest.json'), JSON.stringify(manifest));
|
|
140
140
|
|
|
141
141
|
const report = await runDoctor({ projectRoot: tempDir });
|
|
142
|
-
const check = report.checks.find(c => c.name === 'Layout:
|
|
142
|
+
const check = report.checks.find(c => c.name === 'Layout: no managed files in legacy locations');
|
|
143
143
|
expect(check?.status).toBe('PASS');
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
it('FAIL when
|
|
147
|
-
await mkdir(join(tempDir, '.
|
|
146
|
+
it('FAIL when managed file is registered in a legacy location', async () => {
|
|
147
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
148
148
|
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
149
149
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
150
150
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
@@ -152,7 +152,7 @@ describe('runDoctor', () => {
|
|
|
152
152
|
|
|
153
153
|
await writeFile(join(tempDir, 'infra', 'modules', 'app.ts'), '// @mc-scaffold: domain@1.0.0\n');
|
|
154
154
|
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
155
|
-
await writeFile(join(tempDir, '.
|
|
155
|
+
await writeFile(join(tempDir, '.mc', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
156
156
|
|
|
157
157
|
const manifest = {
|
|
158
158
|
$schema: 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json',
|
|
@@ -163,25 +163,25 @@ describe('runDoctor', () => {
|
|
|
163
163
|
awsRegion: 'eu-north-1',
|
|
164
164
|
enabledModules: ['domain'],
|
|
165
165
|
files: [
|
|
166
|
-
{ path: 'infra/modules/app.ts', module: 'domain', moduleVersion: '1.0.0',
|
|
166
|
+
{ path: '.mc/infra/modules/app.ts', module: 'domain', moduleVersion: '1.0.0',
|
|
167
167
|
sha256: 'abc', wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'managed' as const }
|
|
168
168
|
]
|
|
169
169
|
};
|
|
170
170
|
await writeFile(join(tempDir, '.mc', 'manifest.json'), JSON.stringify(manifest));
|
|
171
171
|
|
|
172
172
|
const report = await runDoctor({ projectRoot: tempDir });
|
|
173
|
-
const check = report.checks.find(c => c.name === 'Layout:
|
|
173
|
+
const check = report.checks.find(c => c.name === 'Layout: no managed files in legacy locations');
|
|
174
174
|
expect(check?.status).toBe('FAIL');
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
it('FAIL when manifest entry points at missing file', async () => {
|
|
178
|
-
await mkdir(join(tempDir, '.
|
|
178
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
179
179
|
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
180
180
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
181
181
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
182
182
|
|
|
183
183
|
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
184
|
-
await writeFile(join(tempDir, '.
|
|
184
|
+
await writeFile(join(tempDir, '.mc', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
185
185
|
|
|
186
186
|
const manifest = {
|
|
187
187
|
$schema: 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json',
|
|
@@ -205,8 +205,8 @@ describe('runDoctor', () => {
|
|
|
205
205
|
});
|
|
206
206
|
|
|
207
207
|
describe('--relocate flag', () => {
|
|
208
|
-
it('
|
|
209
|
-
await mkdir(join(tempDir, '.
|
|
208
|
+
it('does not move scaffold-owned files from the current layout', async () => {
|
|
209
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
210
210
|
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
211
211
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
212
212
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
@@ -215,7 +215,7 @@ describe('runDoctor', () => {
|
|
|
215
215
|
const appContent = '// @mc-scaffold: domain@1.0.0\nimport * as cdk from "aws-cdk-lib";\n';
|
|
216
216
|
await writeFile(join(tempDir, 'infra', 'modules', 'app.ts'), appContent);
|
|
217
217
|
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
218
|
-
await writeFile(join(tempDir, '.
|
|
218
|
+
await writeFile(join(tempDir, '.mc', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
219
219
|
|
|
220
220
|
const { computeChecksumString } = await import('../utils/checksum.js');
|
|
221
221
|
const sha256 = computeChecksumString(appContent);
|
|
@@ -237,25 +237,25 @@ describe('runDoctor', () => {
|
|
|
237
237
|
|
|
238
238
|
const report = await runDoctor({ projectRoot: tempDir, relocate: true });
|
|
239
239
|
|
|
240
|
-
// File
|
|
240
|
+
// File left in its current installed location
|
|
241
241
|
const { existsSync } = await import('node:fs');
|
|
242
|
-
expect(existsSync(join(tempDir, '
|
|
243
|
-
expect(existsSync(join(tempDir, 'infra', 'modules', 'app.ts'))).toBe(false);
|
|
242
|
+
expect(existsSync(join(tempDir, 'infra', 'modules', 'app.ts'))).toBe(true);
|
|
243
|
+
expect(existsSync(join(tempDir, '.mc', 'infra', 'modules', 'app.ts'))).toBe(false);
|
|
244
244
|
|
|
245
245
|
// Summary pass
|
|
246
|
-
const summary = report.checks.find(c => c.name === 'Relocate:
|
|
247
|
-
expect(summary?.status).
|
|
246
|
+
const summary = report.checks.find(c => c.name === 'Relocate: deprecated no-op');
|
|
247
|
+
expect(summary?.status).toBe('PASS');
|
|
248
248
|
});
|
|
249
249
|
|
|
250
250
|
it('leaves hand-edited files in place with warning on --relocate', async () => {
|
|
251
|
-
await mkdir(join(tempDir, '.
|
|
251
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
252
252
|
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
253
253
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
254
254
|
|
|
255
255
|
const originalContent = '// @mc-scaffold: domain@1.0.0\noriginal\n';
|
|
256
256
|
const modifiedContent = '// @mc-scaffold: domain@1.0.0\nmodified by user\n';
|
|
257
257
|
await writeFile(join(tempDir, 'infra', 'modules', 'app.ts'), modifiedContent);
|
|
258
|
-
await writeFile(join(tempDir, '.
|
|
258
|
+
await writeFile(join(tempDir, '.mc', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
259
259
|
|
|
260
260
|
const { computeChecksumString } = await import('../utils/checksum.js');
|
|
261
261
|
const originalSha = computeChecksumString(originalContent); // manifest has original sha
|
|
@@ -280,17 +280,17 @@ describe('runDoctor', () => {
|
|
|
280
280
|
// File left in place
|
|
281
281
|
const { existsSync } = await import('node:fs');
|
|
282
282
|
expect(existsSync(join(tempDir, 'infra', 'modules', 'app.ts'))).toBe(true);
|
|
283
|
-
expect(existsSync(join(tempDir, '.
|
|
283
|
+
expect(existsSync(join(tempDir, '.mc', 'infra', 'modules', 'app.ts'))).toBe(false);
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
-
it('
|
|
287
|
-
await mkdir(join(tempDir, '.
|
|
286
|
+
it('returns PASS when no manifest present on --relocate', async () => {
|
|
287
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
288
288
|
|
|
289
289
|
const report = await runDoctor({ projectRoot: tempDir, relocate: true });
|
|
290
290
|
|
|
291
|
-
expect(report.exitCode).toBe(
|
|
292
|
-
const check = report.checks.find(c => c.name === 'Relocate:
|
|
293
|
-
expect(check?.status).toBe('
|
|
291
|
+
expect(report.exitCode).toBe(0);
|
|
292
|
+
const check = report.checks.find(c => c.name === 'Relocate: deprecated no-op');
|
|
293
|
+
expect(check?.status).toBe('PASS');
|
|
294
294
|
});
|
|
295
295
|
});
|
|
296
296
|
|
|
@@ -299,8 +299,8 @@ describe('runDoctor', () => {
|
|
|
299
299
|
expect(DOCTOR_FIX_FLAG).toBe('--fix');
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
-
it('
|
|
303
|
-
await mkdir(join(tempDir, '.
|
|
302
|
+
it('keeps scaffold-owned files in current layout and leaves manifest paths unchanged', async () => {
|
|
303
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
304
304
|
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
305
305
|
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
306
306
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
@@ -309,7 +309,7 @@ describe('runDoctor', () => {
|
|
|
309
309
|
const appContent = '// @mc-scaffold: domain@1.0.0\nimport * as cdk from "aws-cdk-lib";\n';
|
|
310
310
|
await writeFile(join(tempDir, 'infra', 'modules', 'app.ts'), appContent);
|
|
311
311
|
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
312
|
-
await writeFile(join(tempDir, '.
|
|
312
|
+
await writeFile(join(tempDir, '.mc', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
313
313
|
|
|
314
314
|
const { computeChecksumString } = await import('../utils/checksum.js');
|
|
315
315
|
const sha256 = computeChecksumString(appContent);
|
|
@@ -331,41 +331,35 @@ describe('runDoctor', () => {
|
|
|
331
331
|
|
|
332
332
|
const report = await runDoctor({ projectRoot: tempDir, fix: true });
|
|
333
333
|
|
|
334
|
-
// File
|
|
334
|
+
// File remains in its current installed location
|
|
335
335
|
const { existsSync } = await import('node:fs');
|
|
336
|
-
expect(existsSync(join(tempDir, '
|
|
337
|
-
expect(existsSync(join(tempDir, 'infra', 'modules', 'app.ts'))).toBe(false);
|
|
336
|
+
expect(existsSync(join(tempDir, 'infra', 'modules', 'app.ts'))).toBe(true);
|
|
337
|
+
expect(existsSync(join(tempDir, '.mc', 'infra', 'modules', 'app.ts'))).toBe(false);
|
|
338
338
|
|
|
339
|
-
// Manifest
|
|
339
|
+
// Manifest remains on current installed path
|
|
340
340
|
const manifestOnDisk = JSON.parse(
|
|
341
341
|
await (await import('node:fs/promises')).readFile(join(tempDir, '.mc', 'manifest.json'), 'utf-8')
|
|
342
342
|
);
|
|
343
343
|
const paths = manifestOnDisk.files.map((f: { path: string }) => f.path);
|
|
344
|
-
expect(paths).toContain('
|
|
345
|
-
expect(paths).not.toContain('infra/modules/app.ts');
|
|
344
|
+
expect(paths).toContain('infra/modules/app.ts');
|
|
345
|
+
expect(paths).not.toContain('.mc/infra/modules/app.ts');
|
|
346
346
|
|
|
347
|
-
// Report includes the relocation
|
|
348
|
-
const summary = report.checks.find(c => c.name === 'Relocate:
|
|
347
|
+
// Report includes the relocation no-op check
|
|
348
|
+
const summary = report.checks.find(c => c.name === 'Relocate: deprecated no-op');
|
|
349
349
|
expect(summary).toBeDefined();
|
|
350
|
-
expect(summary?.
|
|
351
|
-
|
|
352
|
-
// Per-file move check
|
|
353
|
-
const moveCheck = report.checks.find(
|
|
354
|
-
c => c.name === 'Relocate: infra/modules/app.ts'
|
|
355
|
-
);
|
|
356
|
-
expect(moveCheck?.status).toBe('PASS');
|
|
350
|
+
expect(summary?.status).toBe('PASS');
|
|
357
351
|
|
|
358
352
|
// Doctor's full report still ran (superset of --relocate)
|
|
359
353
|
expect(report.checks.some(c => c.name === 'Domain configs valid')).toBe(true);
|
|
360
354
|
});
|
|
361
355
|
|
|
362
|
-
it('
|
|
363
|
-
await mkdir(join(tempDir, '.
|
|
356
|
+
it('runs the relocation no-op before the full strict report', async () => {
|
|
357
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
364
358
|
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
365
359
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
366
360
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
367
361
|
|
|
368
|
-
await writeFile(join(tempDir, '.
|
|
362
|
+
await writeFile(join(tempDir, '.mc', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
369
363
|
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
370
364
|
|
|
371
365
|
const manifest = {
|
|
@@ -383,13 +377,12 @@ describe('runDoctor', () => {
|
|
|
383
377
|
const report = await runDoctor({ projectRoot: tempDir, fix: true });
|
|
384
378
|
|
|
385
379
|
// No files moved
|
|
386
|
-
const summary = report.checks.find(c => c.name === 'Relocate:
|
|
380
|
+
const summary = report.checks.find(c => c.name === 'Relocate: deprecated no-op');
|
|
387
381
|
expect(summary).toBeDefined();
|
|
388
|
-
expect(summary?.message).toMatch(/0 file\(s\) moved/);
|
|
389
382
|
expect(summary?.status).toBe('PASS');
|
|
390
383
|
|
|
391
|
-
//
|
|
392
|
-
expect(report.
|
|
384
|
+
// Full strict doctor still runs after the no-op preflight.
|
|
385
|
+
expect(report.checks.some(c => c.name === 'Domain configs valid')).toBe(true);
|
|
393
386
|
});
|
|
394
387
|
});
|
|
395
388
|
|
|
@@ -397,11 +390,11 @@ describe('runDoctor', () => {
|
|
|
397
390
|
/** Build a minimal temp project that passes the W1/W2 baseline checks
|
|
398
391
|
* so the new W5 checks can be evaluated in isolation. */
|
|
399
392
|
async function buildBaseProject(): Promise<void> {
|
|
400
|
-
await mkdir(join(tempDir, '.
|
|
393
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
401
394
|
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
402
395
|
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
403
396
|
await writeFile(
|
|
404
|
-
join(tempDir, '.
|
|
397
|
+
join(tempDir, '.mc', 'scaffold-config.json'),
|
|
405
398
|
JSON.stringify({ domainIds: [] })
|
|
406
399
|
);
|
|
407
400
|
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
ActionBackendAccess,
|
|
4
|
+
type ActionApiExposure,
|
|
5
|
+
type ApiRegistryEntry,
|
|
6
|
+
type DomainRegistry,
|
|
7
|
+
type ActionRegistryEntry,
|
|
8
|
+
} from '@mettlecast/domain-cdk-packer';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Issue #4662 Task D — package freshness self-test.
|
|
12
|
+
*
|
|
13
|
+
* This file is the runtime-side companion to `scripts/check-package-freshness.mjs`.
|
|
14
|
+
* Where the script audits every workspace that consumes a `@mettlecast/*`
|
|
15
|
+
* package, this file pins the CLI's own expectations against the linked
|
|
16
|
+
* `@mettlecast/domain-cdk-packer` types. If a developer hand-edits the
|
|
17
|
+
* linked `dist/` and forgets to rebuild, one of these assertions will
|
|
18
|
+
* fail at `tsc --noEmit` time — long before the published CLI ends up
|
|
19
|
+
* in a scaffolded project.
|
|
20
|
+
*
|
|
21
|
+
* The test also pins every release-blocker field added by the action-first
|
|
22
|
+
* hardening work (#4619 → #4662). Adding a new field to the CDK packer
|
|
23
|
+
* types should make this test fail until the CLI is updated and rebuilt.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
describe('CLI ↔ CDK packer type freshness (#4662 Task D)', () => {
|
|
27
|
+
it('exposes the canonical ActionBackendAccess union', () => {
|
|
28
|
+
// If the linked packer drops or renames a backendAccess variant
|
|
29
|
+
// the assignment below will fail to typecheck. Adding a new variant
|
|
30
|
+
// (e.g. 'workspace' being reintroduced) is a release-blocker for the
|
|
31
|
+
// CLI until the linked packer is rebuilt and re-installed.
|
|
32
|
+
const variants: ActionBackendAccess[] = ['private', 'domain', 'platform'];
|
|
33
|
+
expect(variants).toHaveLength(3);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('exposes the ActionApiExposure.securityException field added for anonymous routes', () => {
|
|
37
|
+
const exposure: ActionApiExposure = {
|
|
38
|
+
type: 'api',
|
|
39
|
+
path: '/v1/health',
|
|
40
|
+
method: 'GET',
|
|
41
|
+
auth: 'none',
|
|
42
|
+
tenancy: 'none',
|
|
43
|
+
securityException: { reason: 'public liveness probe; ticket OPS-123' },
|
|
44
|
+
};
|
|
45
|
+
expect(exposure.securityException?.reason).toMatch(/OPS-123/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('exposes the ApiRegistryEntry.securityException field added in #4662', () => {
|
|
49
|
+
// The build-registry does not populate this field for legacy
|
|
50
|
+
// `defineApi` entries (they don't carry securityException in
|
|
51
|
+
// source), but the registry type must declare it so that
|
|
52
|
+
// future migrations can attach it. This assertion pins the field
|
|
53
|
+
// shape so the CLI doesn't silently fall behind if a future
|
|
54
|
+
// packer refactor removes the declaration.
|
|
55
|
+
const api: ApiRegistryEntry = {
|
|
56
|
+
kind: 'api',
|
|
57
|
+
handlerFile: 'src/api.ts',
|
|
58
|
+
path: '/v1/health',
|
|
59
|
+
method: 'GET',
|
|
60
|
+
authType: 'none',
|
|
61
|
+
securityException: { reason: 'public liveness probe' },
|
|
62
|
+
};
|
|
63
|
+
expect(api.securityException?.reason).toMatch(/liveness/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('exposes the ActionRegistryEntry shape with the latest exposure metadata flags', () => {
|
|
67
|
+
const action: ActionRegistryEntry = {
|
|
68
|
+
kind: 'action',
|
|
69
|
+
handlerFile: 'src/actions/do-thing.ts',
|
|
70
|
+
backendAccess: 'domain',
|
|
71
|
+
exposure: { type: 'internal' },
|
|
72
|
+
idempotent: false,
|
|
73
|
+
};
|
|
74
|
+
// The new optional `exposureDeclared` flag carried over from the
|
|
75
|
+
// CLI validator must be present on the type — otherwise the CLI's
|
|
76
|
+
// own `runValidate` cannot detect defaulting regressions.
|
|
77
|
+
expect(action).toHaveProperty('exposure');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('exposes the DomainRegistry schema with the latest fields', () => {
|
|
81
|
+
const registry: DomainRegistry = {
|
|
82
|
+
schemaVersion: '1',
|
|
83
|
+
domainRoot: '/tmp/fake',
|
|
84
|
+
domain: { kind: 'domain', id: 'test', name: 'Test', tenancy: 'required' },
|
|
85
|
+
apis: [],
|
|
86
|
+
webhooks: [],
|
|
87
|
+
subscribers: [],
|
|
88
|
+
schedules: [],
|
|
89
|
+
jobs: [],
|
|
90
|
+
actions: [],
|
|
91
|
+
integrations: [],
|
|
92
|
+
events: [],
|
|
93
|
+
};
|
|
94
|
+
expect(registry.schemaVersion).toBe('1');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does not pin to a fixed @mettlecast/domain-cdk-packer version in package.json', async () => {
|
|
98
|
+
// Read the CLI's own package.json and assert that the reference to
|
|
99
|
+
// `@mettlecast/domain-cdk-packer` is a workspace / `*` range. The
|
|
100
|
+
// publish workflow copies the freshly built packer `dist/` into the
|
|
101
|
+
// CLI's node_modules before compile, so the CLI must not depend on
|
|
102
|
+
// a published version.
|
|
103
|
+
const fs = await import('node:fs/promises');
|
|
104
|
+
const path = await import('node:path');
|
|
105
|
+
const url = await import('node:url');
|
|
106
|
+
const here = url.fileURLToPath(import.meta.url);
|
|
107
|
+
const cliPkgJsonPath = path.join(path.dirname(here), '..', '..', 'package.json');
|
|
108
|
+
const raw = await fs.readFile(cliPkgJsonPath, 'utf8');
|
|
109
|
+
const parsed = JSON.parse(raw);
|
|
110
|
+
const range = parsed.dependencies?.['@mettlecast/domain-cdk-packer']
|
|
111
|
+
?? parsed.devDependencies?.['@mettlecast/domain-cdk-packer'];
|
|
112
|
+
expect(['*', 'workspace:*']).toContain(range);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -10,11 +10,11 @@ const backendLambdaDir = join(repoRoot, 'scaffold-src', 'modules', 'domain');
|
|
|
10
10
|
const coreDir = join(repoRoot, 'scaffold-src', 'modules', 'core');
|
|
11
11
|
|
|
12
12
|
describe('scaffold-src layout (Part A)', () => {
|
|
13
|
-
it('domain
|
|
14
|
-
expect(existsSync(join(backendLambdaDir, '.mc/infra
|
|
13
|
+
it('domain does not carry removed .mc/infra scaffold files', () => {
|
|
14
|
+
expect(existsSync(join(backendLambdaDir, '.mc/infra'))).toBe(false);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
it('domain infra/
|
|
17
|
+
it('domain does not carry module-local infra/ files', () => {
|
|
18
18
|
expect(existsSync(join(backendLambdaDir, 'infra'))).toBe(false);
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -22,17 +22,17 @@ describe('scaffold-src layout (Part A)', () => {
|
|
|
22
22
|
expect(existsSync(join(backendLambdaDir, 'cdk.json.tpl'))).toBe(true);
|
|
23
23
|
const content = readFileSync(join(backendLambdaDir, 'cdk.json.tpl'), 'utf-8');
|
|
24
24
|
const parsed = JSON.parse(content);
|
|
25
|
-
expect(parsed.app).toContain('
|
|
25
|
+
expect(parsed.app).toContain('infra/modules/app.ts');
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
it('core
|
|
29
|
-
expect(existsSync(join(coreDir, '.mc/workflows
|
|
28
|
+
it('core does not carry removed .mc workflow templates', () => {
|
|
29
|
+
expect(existsSync(join(coreDir, '.mc/workflows'))).toBe(false);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
it('core has
|
|
33
|
-
expect(existsSync(join(coreDir, '.github/workflows/
|
|
34
|
-
const content = readFileSync(join(coreDir, '.github/workflows/
|
|
35
|
-
expect(content).toContain('
|
|
32
|
+
it('core has current validate-domain workflow', () => {
|
|
33
|
+
expect(existsSync(join(coreDir, '.github/workflows/validate-domain.yml'))).toBe(true);
|
|
34
|
+
const content = readFileSync(join(coreDir, '.github/workflows/validate-domain.yml'), 'utf-8');
|
|
35
|
+
expect(content).toContain('Validate domain primitives');
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
});
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
describe('package-scaffold policy inference', () => {
|
|
4
4
|
function inferPolicyFromPath(filePath: string): string {
|
|
5
|
-
if (/^infra\//.test(filePath) ||
|
|
5
|
+
if (/^infra\//.test(filePath) ||
|
|
6
6
|
filePath === 'mc-deploy.yml' || /^\.github\/workflows\//.test(filePath)) {
|
|
7
7
|
return 'managed';
|
|
8
8
|
}
|
|
@@ -18,10 +18,10 @@ describe('package-scaffold policy inference', () => {
|
|
|
18
18
|
return 'editable';
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
it('returns managed for
|
|
22
|
-
expect(inferPolicyFromPath('
|
|
23
|
-
expect(inferPolicyFromPath('
|
|
24
|
-
expect(inferPolicyFromPath('
|
|
21
|
+
it('returns managed for infra/ paths', () => {
|
|
22
|
+
expect(inferPolicyFromPath('infra/modules/app.ts')).toBe('managed');
|
|
23
|
+
expect(inferPolicyFromPath('infra/cdk.json')).toBe('managed');
|
|
24
|
+
expect(inferPolicyFromPath('infra/tsconfig.json')).toBe('managed');
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it('returns managed for .github/workflows/ paths', () => {
|
|
@@ -157,8 +157,8 @@ describe('installScaffoldFile', () => {
|
|
|
157
157
|
|
|
158
158
|
const result = await installScaffoldFile(filePath, newContent, 'editable', currentEntry);
|
|
159
159
|
expect(result.status).toBe('update-available');
|
|
160
|
-
// Verify NO .
|
|
161
|
-
await expect(access(filePath + '.
|
|
160
|
+
// Verify NO .mc-upgrade file written
|
|
161
|
+
await expect(access(filePath + '.mc-upgrade')).rejects.toThrow();
|
|
162
162
|
// Verify original file NOT overwritten
|
|
163
163
|
const diskContent = await readFile(filePath, 'utf-8');
|
|
164
164
|
expect(diskContent).toBe(existingContent);
|
|
@@ -57,8 +57,8 @@ describe('manifest utilities', () => {
|
|
|
57
57
|
expect(inferPolicyFromPath('infra/main.tf')).toBe('managed');
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
it('returns editable for .mc
|
|
61
|
-
expect(inferPolicyFromPath('.mc/
|
|
60
|
+
it('returns editable for unrecognised .mc paths', () => {
|
|
61
|
+
expect(inferPolicyFromPath('.mc/custom/stack.ts')).toBe('editable');
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
it('returns managed for mc-deploy.yml', () => {
|