@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.
Files changed (49) hide show
  1. package/dist/builder/build-registry.js +128 -13
  2. package/dist/builder/load-module.js +3 -3
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/build-catalog.d.ts +2 -2
  5. package/dist/commands/build-catalog.js +5 -5
  6. package/dist/commands/build-flows.js +1 -1
  7. package/dist/commands/build.js +5 -5
  8. package/dist/commands/check-hashes.js +2 -2
  9. package/dist/commands/create-project.js +1 -1
  10. package/dist/commands/doctor.d.ts +5 -7
  11. package/dist/commands/doctor.js +44 -165
  12. package/dist/commands/power-tune.js +2 -2
  13. package/dist/commands/show-dns.d.ts +1 -1
  14. package/dist/commands/show-dns.js +5 -5
  15. package/dist/commands/upgrade.js +17 -11
  16. package/dist/commands/validate.js +183 -0
  17. package/dist/utils/header-inject.js +2 -2
  18. package/dist/utils/install-file.d.ts +1 -1
  19. package/dist/utils/install-file.js +1 -1
  20. package/dist/utils/manifest.js +1 -2
  21. package/dist/utils/scaffold-config.d.ts +2 -2
  22. package/dist/utils/scaffold-config.js +1 -1
  23. package/package.json +1 -1
  24. package/src/__tests__/commands/check-hashes.test.ts +9 -9
  25. package/src/__tests__/commands/upgrade.test.ts +7 -7
  26. package/src/__tests__/doctor.test.ts +60 -67
  27. package/src/__tests__/package-freshness.test.ts +114 -0
  28. package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
  29. package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
  30. package/src/__tests__/utils/install-file.test.ts +2 -2
  31. package/src/__tests__/utils/manifest.test.ts +2 -2
  32. package/src/__tests__/validate.test.ts +652 -1
  33. package/src/builder/build-registry.ts +147 -15
  34. package/src/builder/load-module.ts +3 -3
  35. package/src/cli.ts +3 -3
  36. package/src/commands/build-catalog.ts +5 -5
  37. package/src/commands/build-flows.ts +1 -1
  38. package/src/commands/build.ts +5 -5
  39. package/src/commands/check-hashes.ts +2 -2
  40. package/src/commands/create-project.ts +1 -1
  41. package/src/commands/doctor.ts +52 -181
  42. package/src/commands/power-tune.ts +2 -2
  43. package/src/commands/show-dns.ts +5 -5
  44. package/src/commands/upgrade.ts +16 -10
  45. package/src/commands/validate.ts +226 -1
  46. package/src/utils/header-inject.ts +2 -2
  47. package/src/utils/install-file.ts +1 -1
  48. package/src/utils/manifest.ts +1 -2
  49. 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, '.tib'), { recursive: true });
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, '.tib', 'scaffold-config.json'),
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, '.tib'), { recursive: true });
45
+ await mkdir(join(tempDir, '.mc'), { recursive: true });
46
46
  await writeFile(
47
- join(tempDir, '.tib', 'scaffold-config.json'),
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, '.tib'), { recursive: true });
59
+ await mkdir(join(tempDir, '.mc'), { recursive: true });
60
60
  await writeFile(
61
- join(tempDir, '.tib', 'scaffold-config.json'),
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, '.tib'), { recursive: true });
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, '.tib', 'scaffold-config.json'),
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, '.tib'), { recursive: true });
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, '.tib', 'scaffold-config.json'),
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 owned files in .mc/', async () => {
116
- await mkdir(join(tempDir, '.tib'), { recursive: true });
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, '.tib', 'infra', 'modules'), { recursive: true });
120
+ await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
121
121
 
122
- await writeFile(join(tempDir, '.tib', 'infra', 'modules', 'app.ts'), '// @mc-scaffold: domain@1.0.0\n');
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, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
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: '.mc/infra/modules/app.ts', module: 'domain', moduleVersion: '1.0.0',
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: owned files in .mc/');
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 owned file is outside .mc/ (old layout)', async () => {
147
- await mkdir(join(tempDir, '.tib'), { recursive: true });
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, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
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: owned files in .mc/');
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, '.tib'), { recursive: true });
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, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
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('moves scaffold-owned files from old layout to .mc/', async () => {
209
- await mkdir(join(tempDir, '.tib'), { recursive: true });
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, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
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 moved
240
+ // File left in its current installed location
241
241
  const { existsSync } = await import('node:fs');
242
- expect(existsSync(join(tempDir, '.mc', 'infra', 'modules', 'app.ts'))).toBe(true);
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: summary');
247
- expect(summary?.status).toMatch(/PASS|WARN/);
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, '.tib'), { recursive: true });
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, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
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, '.tib', 'infra', 'modules', 'app.ts'))).toBe(false);
283
+ expect(existsSync(join(tempDir, '.mc', 'infra', 'modules', 'app.ts'))).toBe(false);
284
284
  });
285
285
 
286
- it('FAIL when no manifest present on --relocate', async () => {
287
- await mkdir(join(tempDir, '.tib'), { recursive: true });
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(1);
292
- const check = report.checks.find(c => c.name === 'Relocate: manifest present');
293
- expect(check?.status).toBe('FAIL');
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('moves scaffold-owned files from old layout to .mc/ and updates manifest', async () => {
303
- await mkdir(join(tempDir, '.tib'), { recursive: true });
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, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
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 moved from old layout to .mc/
334
+ // File remains in its current installed location
335
335
  const { existsSync } = await import('node:fs');
336
- expect(existsSync(join(tempDir, '.mc', 'infra', 'modules', 'app.ts'))).toBe(true);
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 updated: old path removed, new path added
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('.mc/infra/modules/app.ts');
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 summary
348
- const summary = report.checks.find(c => c.name === 'Relocate: summary');
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?.message).toMatch(/1 file\(s\) moved/);
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('is a no-op on a clean project (exit 0, 0 files moved)', async () => {
363
- await mkdir(join(tempDir, '.tib'), { recursive: true });
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, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
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: summary');
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
- // Clean exit (no FAIL anywhere in the report)
392
- expect(report.exitCode).toBe(0);
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, '.tib'), { recursive: true });
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, '.tib', 'scaffold-config.json'),
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 has app.ts.tpl under .mc/infra/modules/', () => {
14
- expect(existsSync(join(backendLambdaDir, '.mc/infra/modules/app.ts.tpl'))).toBe(true);
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/ directory is removed', () => {
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('.mc/infra/modules/app.ts');
25
+ expect(parsed.app).toContain('infra/modules/app.ts');
26
26
  });
27
27
 
28
- it('core has .mc/workflows/ci.yml.tpl', () => {
29
- expect(existsSync(join(coreDir, '.mc/workflows/ci.yml.tpl'))).toBe(true);
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 .github/workflows/ci.yml seed stub', () => {
33
- expect(existsSync(join(coreDir, '.github/workflows/ci.yml'))).toBe(true);
34
- const content = readFileSync(join(coreDir, '.github/workflows/ci.yml'), 'utf-8');
35
- expect(content).toContain('tib-ci.yml');
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) || /^\.tib\/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 .mc/infra/ paths', () => {
22
- expect(inferPolicyFromPath('.mc/infra/modules/app.ts')).toBe('managed');
23
- expect(inferPolicyFromPath('.mc/infra/cdk.json')).toBe('managed');
24
- expect(inferPolicyFromPath('.mc/infra/tsconfig.json')).toBe('managed');
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 .tib-upgrade file written
161
- await expect(access(filePath + '.tib-upgrade')).rejects.toThrow();
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/infra/ paths', () => {
61
- expect(inferPolicyFromPath('.mc/infra/stack.ts')).toBe('editable');
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', () => {