@mettlecast/domain-cli 0.2.0
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/CHANGELOG.md +18 -0
- package/README.md +133 -0
- package/dist/builder/build-registry.d.ts +20 -0
- package/dist/builder/build-registry.js +178 -0
- package/dist/builder/build-types.d.ts +10 -0
- package/dist/builder/build-types.js +32 -0
- package/dist/builder/load-domain-module-config.d.ts +12 -0
- package/dist/builder/load-domain-module-config.js +34 -0
- package/dist/builder/load-module.d.ts +26 -0
- package/dist/builder/load-module.js +114 -0
- package/dist/builder/zod-to-json.d.ts +10 -0
- package/dist/builder/zod-to-json.js +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +338 -0
- package/dist/commands/add-api.d.ts +32 -0
- package/dist/commands/add-api.js +106 -0
- package/dist/commands/add-component.d.ts +15 -0
- package/dist/commands/add-component.js +78 -0
- package/dist/commands/add-domain.d.ts +21 -0
- package/dist/commands/add-domain.js +71 -0
- package/dist/commands/add-flow.d.ts +14 -0
- package/dist/commands/add-flow.js +108 -0
- package/dist/commands/add-hook.d.ts +15 -0
- package/dist/commands/add-hook.js +65 -0
- package/dist/commands/add-migration.d.ts +26 -0
- package/dist/commands/add-migration.js +93 -0
- package/dist/commands/add-module.d.ts +7 -0
- package/dist/commands/add-module.js +261 -0
- package/dist/commands/add-page.d.ts +13 -0
- package/dist/commands/add-page.js +170 -0
- package/dist/commands/add-seed-page.d.ts +5 -0
- package/dist/commands/add-seed-page.js +251 -0
- package/dist/commands/add-subscriber.d.ts +30 -0
- package/dist/commands/add-subscriber.js +87 -0
- package/dist/commands/add-test.d.ts +22 -0
- package/dist/commands/add-test.js +285 -0
- package/dist/commands/audit.d.ts +39 -0
- package/dist/commands/audit.js +171 -0
- package/dist/commands/build-catalog.d.ts +68 -0
- package/dist/commands/build-catalog.js +105 -0
- package/dist/commands/build-flows.d.ts +26 -0
- package/dist/commands/build-flows.js +174 -0
- package/dist/commands/build.d.ts +17 -0
- package/dist/commands/build.js +39 -0
- package/dist/commands/check-hashes.d.ts +38 -0
- package/dist/commands/check-hashes.js +97 -0
- package/dist/commands/create-project.d.ts +5 -0
- package/dist/commands/create-project.js +272 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +27 -0
- package/dist/commands/doctor.d.ts +48 -0
- package/dist/commands/doctor.js +1301 -0
- package/dist/commands/explain.d.ts +34 -0
- package/dist/commands/explain.js +167 -0
- package/dist/commands/power-tune.d.ts +1 -0
- package/dist/commands/power-tune.js +87 -0
- package/dist/commands/reseed-page.d.ts +6 -0
- package/dist/commands/reseed-page.js +297 -0
- package/dist/commands/show-dns.d.ts +15 -0
- package/dist/commands/show-dns.js +101 -0
- package/dist/commands/show.d.ts +46 -0
- package/dist/commands/show.js +135 -0
- package/dist/commands/test.d.ts +18 -0
- package/dist/commands/test.js +56 -0
- package/dist/commands/upgrade-backend.d.ts +9 -0
- package/dist/commands/upgrade-backend.js +46 -0
- package/dist/commands/upgrade-event.d.ts +22 -0
- package/dist/commands/upgrade-event.js +111 -0
- package/dist/commands/upgrade-frontend.d.ts +5 -0
- package/dist/commands/upgrade-frontend.js +99 -0
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +495 -0
- package/dist/commands/validate.d.ts +30 -0
- package/dist/commands/validate.js +148 -0
- package/dist/runtime-stubs/hydrate-local-ctx.d.ts +49 -0
- package/dist/runtime-stubs/hydrate-local-ctx.js +45 -0
- package/dist/runtime-stubs/load-secrets.d.ts +11 -0
- package/dist/runtime-stubs/load-secrets.js +37 -0
- package/dist/server/api-server.d.ts +21 -0
- package/dist/server/api-server.js +14 -0
- package/dist/server/mount-routes.d.ts +19 -0
- package/dist/server/mount-routes.js +42 -0
- package/dist/templates/api-skeleton.d.ts +18 -0
- package/dist/templates/api-skeleton.js +69 -0
- package/dist/templates/claude-md.d.ts +4 -0
- package/dist/templates/claude-md.js +7 -0
- package/dist/templates/dashboard-pages/index.d.ts +101 -0
- package/dist/templates/dashboard-pages/index.js +22 -0
- package/dist/templates/domain-config.d.ts +10 -0
- package/dist/templates/domain-config.js +19 -0
- package/dist/templates/events-skeleton.d.ts +9 -0
- package/dist/templates/events-skeleton.js +23 -0
- package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
- package/dist/templates/patterns/api/create-with-event.js +69 -0
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +99 -0
- package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
- package/dist/templates/patterns/api/paginated-list.js +71 -0
- package/dist/templates/patterns/api/simple-crud.d.ts +6 -0
- package/dist/templates/patterns/api/simple-crud.js +95 -0
- package/dist/templates/patterns/api/system-admin.d.ts +5 -0
- package/dist/templates/patterns/api/system-admin.js +98 -0
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +125 -0
- package/dist/templates/patterns/component/index.d.ts +25 -0
- package/dist/templates/patterns/component/index.js +814 -0
- package/dist/templates/patterns/flow/autonomous-remediation.d.ts +5 -0
- package/dist/templates/patterns/flow/autonomous-remediation.js +98 -0
- package/dist/templates/patterns/flow/fan-out-reaction.d.ts +5 -0
- package/dist/templates/patterns/flow/fan-out-reaction.js +53 -0
- package/dist/templates/patterns/flow/human-in-loop.d.ts +5 -0
- package/dist/templates/patterns/flow/human-in-loop.js +90 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.d.ts +5 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.js +67 -0
- package/dist/templates/patterns/hook/index.d.ts +7 -0
- package/dist/templates/patterns/hook/index.js +378 -0
- package/dist/templates/patterns/migration/column-add.d.ts +5 -0
- package/dist/templates/patterns/migration/column-add.js +52 -0
- package/dist/templates/patterns/migration/event-store.d.ts +6 -0
- package/dist/templates/patterns/migration/event-store.js +97 -0
- package/dist/templates/patterns/migration/index.d.ts +5 -0
- package/dist/templates/patterns/migration/index.js +52 -0
- package/dist/templates/patterns/migration/rls-policy.d.ts +5 -0
- package/dist/templates/patterns/migration/rls-policy.js +78 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.d.ts +6 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.js +81 -0
- package/dist/templates/patterns/page/dashboard.d.ts +4 -0
- package/dist/templates/patterns/page/dashboard.js +164 -0
- package/dist/templates/patterns/page/detail.d.ts +4 -0
- package/dist/templates/patterns/page/detail.js +162 -0
- package/dist/templates/patterns/page/form.d.ts +4 -0
- package/dist/templates/patterns/page/form.js +192 -0
- package/dist/templates/patterns/page/index.d.ts +8 -0
- package/dist/templates/patterns/page/index.js +14 -0
- package/dist/templates/patterns/page/list.d.ts +4 -0
- package/dist/templates/patterns/page/list.js +138 -0
- package/dist/templates/patterns/page/settings.d.ts +4 -0
- package/dist/templates/patterns/page/settings.js +318 -0
- package/dist/templates/patterns/page/wizard.d.ts +4 -0
- package/dist/templates/patterns/page/wizard.js +293 -0
- package/dist/templates/patterns/subscriber/audit-relay.d.ts +5 -0
- package/dist/templates/patterns/subscriber/audit-relay.js +51 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.d.ts +5 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.js +60 -0
- package/dist/templates/patterns/subscriber/single-step-projection.d.ts +5 -0
- package/dist/templates/patterns/subscriber/single-step-projection.js +36 -0
- package/dist/templates/readme.d.ts +4 -0
- package/dist/templates/readme.js +7 -0
- package/dist/templates/subscriber-skeleton.d.ts +11 -0
- package/dist/templates/subscriber-skeleton.js +23 -0
- package/dist/utils/checksum.d.ts +18 -0
- package/dist/utils/checksum.js +39 -0
- package/dist/utils/file-helpers.d.ts +35 -0
- package/dist/utils/file-helpers.js +59 -0
- package/dist/utils/header-inject.d.ts +15 -0
- package/dist/utils/header-inject.js +93 -0
- package/dist/utils/install-file.d.ts +17 -0
- package/dist/utils/install-file.js +78 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/manifest.d.ts +28 -0
- package/dist/utils/manifest.js +93 -0
- package/dist/utils/s3-fetch.d.ts +25 -0
- package/dist/utils/s3-fetch.js +52 -0
- package/dist/utils/scaffold-config.d.ts +95 -0
- package/dist/utils/scaffold-config.js +53 -0
- package/dist/utils/token-substitution.d.ts +35 -0
- package/dist/utils/token-substitution.js +52 -0
- package/dist/utils/zod-extractor.d.ts +10 -0
- package/dist/utils/zod-extractor.js +28 -0
- package/package.json +43 -0
- package/src/__tests__/build-registry.test.ts +115 -0
- package/src/__tests__/build-types.test.ts +100 -0
- package/src/__tests__/builder/walkDomainDir.test.ts +140 -0
- package/src/__tests__/commands/add-domain.test.ts +174 -0
- package/src/__tests__/commands/add-flow.test.ts +69 -0
- package/src/__tests__/commands/add-module.test.ts +207 -0
- package/src/__tests__/commands/build-flows.test.ts +154 -0
- package/src/__tests__/commands/check-hashes.test.ts +142 -0
- package/src/__tests__/commands/create-project.test.ts +712 -0
- package/src/__tests__/commands/upgrade.test.ts +762 -0
- package/src/__tests__/doctor.test.ts +291 -0
- package/src/__tests__/hydrate-local-ctx.test.ts +71 -0
- package/src/__tests__/load-domain-module-config.test.ts +10 -0
- package/src/__tests__/mount-routes.test.ts +100 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +46 -0
- package/src/__tests__/scripts/package-scaffold.test.ts +45 -0
- package/src/__tests__/utils/checksum.test.ts +90 -0
- package/src/__tests__/utils/header-inject.test.ts +89 -0
- package/src/__tests__/utils/install-file.test.ts +207 -0
- package/src/__tests__/utils/manifest.test.ts +229 -0
- package/src/__tests__/utils/token-substitution.test.ts +134 -0
- package/src/__tests__/validate.test.ts +18 -0
- package/src/builder/build-registry.ts +237 -0
- package/src/builder/build-types.ts +36 -0
- package/src/builder/load-domain-module-config.ts +43 -0
- package/src/builder/load-module.ts +133 -0
- package/src/builder/zod-to-json.ts +16 -0
- package/src/cli.ts +387 -0
- package/src/commands/add-api.ts +145 -0
- package/src/commands/add-component.ts +116 -0
- package/src/commands/add-domain.ts +100 -0
- package/src/commands/add-flow.ts +136 -0
- package/src/commands/add-hook.ts +100 -0
- package/src/commands/add-migration.ts +129 -0
- package/src/commands/add-module.ts +347 -0
- package/src/commands/add-page.ts +242 -0
- package/src/commands/add-seed-page.ts +338 -0
- package/src/commands/add-subscriber.ts +123 -0
- package/src/commands/add-test.ts +328 -0
- package/src/commands/audit.ts +217 -0
- package/src/commands/build-catalog.ts +181 -0
- package/src/commands/build-flows.ts +198 -0
- package/src/commands/build.ts +59 -0
- package/src/commands/check-hashes.ts +150 -0
- package/src/commands/create-project.ts +367 -0
- package/src/commands/dev.ts +47 -0
- package/src/commands/doctor.ts +1362 -0
- package/src/commands/explain.ts +209 -0
- package/src/commands/power-tune.ts +112 -0
- package/src/commands/reseed-page.ts +410 -0
- package/src/commands/show-dns.ts +139 -0
- package/src/commands/show.ts +179 -0
- package/src/commands/test.ts +91 -0
- package/src/commands/upgrade-backend.ts +72 -0
- package/src/commands/upgrade-event.ts +175 -0
- package/src/commands/upgrade-frontend.ts +135 -0
- package/src/commands/upgrade.ts +668 -0
- package/src/commands/validate.ts +208 -0
- package/src/runtime-stubs/hydrate-local-ctx.ts +102 -0
- package/src/runtime-stubs/load-secrets.ts +40 -0
- package/src/server/api-server.ts +34 -0
- package/src/server/mount-routes.ts +68 -0
- package/src/templates/api-skeleton.ts +72 -0
- package/src/templates/claude-md.ts +7 -0
- package/src/templates/dashboard-pages/account/api-keys.tsx +107 -0
- package/src/templates/dashboard-pages/account/audit-log.tsx +60 -0
- package/src/templates/dashboard-pages/account/index.ts +5 -0
- package/src/templates/dashboard-pages/account/members.tsx +107 -0
- package/src/templates/dashboard-pages/account/profile.tsx +53 -0
- package/src/templates/dashboard-pages/account/workspace-settings.tsx +64 -0
- package/src/templates/dashboard-pages/auth/accept-invitation.tsx +62 -0
- package/src/templates/dashboard-pages/auth/choose-org.tsx +66 -0
- package/src/templates/dashboard-pages/auth/forgot-password.tsx +69 -0
- package/src/templates/dashboard-pages/auth/index.ts +10 -0
- package/src/templates/dashboard-pages/auth/login.tsx +75 -0
- package/src/templates/dashboard-pages/auth/mfa-setup.tsx +56 -0
- package/src/templates/dashboard-pages/auth/mfa-verify.tsx +49 -0
- package/src/templates/dashboard-pages/auth/reset-password.tsx +64 -0
- package/src/templates/dashboard-pages/auth/sign-out.tsx +20 -0
- package/src/templates/dashboard-pages/auth/signup.tsx +71 -0
- package/src/templates/dashboard-pages/auth/verify-email.tsx +53 -0
- package/src/templates/dashboard-pages/index.ts +22 -0
- package/src/templates/dashboard-pages/shell/dashboard-home.tsx +41 -0
- package/src/templates/dashboard-pages/shell/forbidden.tsx +46 -0
- package/src/templates/dashboard-pages/shell/index.ts +5 -0
- package/src/templates/dashboard-pages/shell/maintenance.tsx +27 -0
- package/src/templates/dashboard-pages/shell/not-found.tsx +34 -0
- package/src/templates/dashboard-pages/shell/server-error.tsx +44 -0
- package/src/templates/domain-config.ts +20 -0
- package/src/templates/events-skeleton.ts +24 -0
- package/src/templates/patterns/api/create-with-event.ts +72 -0
- package/src/templates/patterns/api/idempotent-mutation.ts +102 -0
- package/src/templates/patterns/api/paginated-list.ts +74 -0
- package/src/templates/patterns/api/simple-crud.ts +100 -0
- package/src/templates/patterns/api/system-admin.ts +101 -0
- package/src/templates/patterns/api/webhook-receiver-style.ts +128 -0
- package/src/templates/patterns/component/index.ts +833 -0
- package/src/templates/patterns/flow/autonomous-remediation.ts +101 -0
- package/src/templates/patterns/flow/fan-out-reaction.ts +56 -0
- package/src/templates/patterns/flow/human-in-loop.ts +93 -0
- package/src/templates/patterns/flow/multi-step-orchestration.ts +70 -0
- package/src/templates/patterns/hook/index.ts +382 -0
- package/src/templates/patterns/migration/column-add.ts +54 -0
- package/src/templates/patterns/migration/event-store.ts +99 -0
- package/src/templates/patterns/migration/index.ts +54 -0
- package/src/templates/patterns/migration/rls-policy.ts +80 -0
- package/src/templates/patterns/migration/tenant-scoped-table.ts +83 -0
- package/src/templates/patterns/page/dashboard.ts +164 -0
- package/src/templates/patterns/page/detail.ts +162 -0
- package/src/templates/patterns/page/form.ts +192 -0
- package/src/templates/patterns/page/index.ts +17 -0
- package/src/templates/patterns/page/list.ts +138 -0
- package/src/templates/patterns/page/settings.ts +318 -0
- package/src/templates/patterns/page/wizard.ts +293 -0
- package/src/templates/patterns/subscriber/audit-relay.ts +54 -0
- package/src/templates/patterns/subscriber/cascade-deletion.ts +63 -0
- package/src/templates/patterns/subscriber/single-step-projection.ts +39 -0
- package/src/templates/readme.ts +7 -0
- package/src/templates/subscriber-skeleton.ts +28 -0
- package/src/utils/checksum.ts +43 -0
- package/src/utils/file-helpers.ts +91 -0
- package/src/utils/header-inject.ts +126 -0
- package/src/utils/install-file.ts +99 -0
- package/src/utils/logger.ts +26 -0
- package/src/utils/manifest.ts +137 -0
- package/src/utils/s3-fetch.ts +91 -0
- package/src/utils/scaffold-config.ts +172 -0
- package/src/utils/token-substitution.ts +80 -0
- package/src/utils/zod-extractor.ts +31 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { runDoctor } from '../commands/doctor.js';
|
|
6
|
+
|
|
7
|
+
describe('runDoctor', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tempDir = await mkdtemp(join(tmpdir(), 'doctor-test-'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
try {
|
|
16
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
17
|
+
} catch {
|
|
18
|
+
// Cleanup best-effort
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns a DoctorReport with checks and exitCode', async () => {
|
|
23
|
+
// Create minimal project structure
|
|
24
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
25
|
+
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
26
|
+
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
27
|
+
|
|
28
|
+
await writeFile(
|
|
29
|
+
join(tempDir, '.tib', 'scaffold-config.json'),
|
|
30
|
+
JSON.stringify({ domainIds: [] })
|
|
31
|
+
);
|
|
32
|
+
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh\necho "hook"');
|
|
33
|
+
|
|
34
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
35
|
+
|
|
36
|
+
expect(report.checks).toBeDefined();
|
|
37
|
+
expect(Array.isArray(report.checks)).toBe(true);
|
|
38
|
+
expect(report.checks.length).toBeGreaterThan(0);
|
|
39
|
+
expect(report.exitCode).toBeDefined();
|
|
40
|
+
expect(typeof report.exitCode).toBe('number');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('reports FAIL when domain config is missing', async () => {
|
|
44
|
+
await mkdir(join(tempDir, 'domains', 'test-domain'), { recursive: true });
|
|
45
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
46
|
+
await writeFile(
|
|
47
|
+
join(tempDir, '.tib', 'scaffold-config.json'),
|
|
48
|
+
JSON.stringify({ domainIds: ['test-domain'] })
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
52
|
+
|
|
53
|
+
const domainCheck = report.checks.find(c => c.name === 'Domain configs valid');
|
|
54
|
+
expect(domainCheck?.status).toBe('FAIL');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('exits with code 1 when FAIL present (always strict)', async () => {
|
|
58
|
+
await mkdir(join(tempDir, 'domains', 'bad-domain'), { recursive: true });
|
|
59
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
60
|
+
await writeFile(
|
|
61
|
+
join(tempDir, '.tib', 'scaffold-config.json'),
|
|
62
|
+
JSON.stringify({ domainIds: ['bad-domain'] })
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
66
|
+
|
|
67
|
+
expect(report.exitCode).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('reports PASS for Husky when hook exists', async () => {
|
|
71
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
72
|
+
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
73
|
+
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
74
|
+
await writeFile(
|
|
75
|
+
join(tempDir, '.tib', 'scaffold-config.json'),
|
|
76
|
+
JSON.stringify({ domainIds: [] })
|
|
77
|
+
);
|
|
78
|
+
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
79
|
+
|
|
80
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
81
|
+
|
|
82
|
+
const huskyCheck = report.checks.find(c => c.name === 'Husky hooks installed');
|
|
83
|
+
expect(huskyCheck?.status).toMatch(/PASS|WARN/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('includes all 15 checks in the report', async () => {
|
|
87
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
88
|
+
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
89
|
+
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
90
|
+
await writeFile(
|
|
91
|
+
join(tempDir, '.tib', 'scaffold-config.json'),
|
|
92
|
+
JSON.stringify({ domainIds: [] })
|
|
93
|
+
);
|
|
94
|
+
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
95
|
+
|
|
96
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
97
|
+
|
|
98
|
+
const expectedChecks = [
|
|
99
|
+
'Domain configs valid',
|
|
100
|
+
'No raw AWS SDK in domains',
|
|
101
|
+
'No cross-domain imports',
|
|
102
|
+
'APIs declare versions',
|
|
103
|
+
'APIs declare tenancy',
|
|
104
|
+
'scaffold-config.json matches on-disk',
|
|
105
|
+
'Husky hooks installed',
|
|
106
|
+
'No root-level flows (deprecated)',
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const expectedName of expectedChecks) {
|
|
110
|
+
expect(report.checks.some(c => c.name === expectedName)).toBe(true);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('layout validation checks', () => {
|
|
115
|
+
it('PASS when manifest has owned files in .mc/', async () => {
|
|
116
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
117
|
+
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
118
|
+
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
119
|
+
await mkdir(join(tempDir, '.tib', 'infra', 'modules'), { recursive: true });
|
|
120
|
+
|
|
121
|
+
await writeFile(join(tempDir, '.tib', 'infra', 'modules', 'app.ts'), '// @mc-scaffold: backend-lambda@1.0.0\n');
|
|
122
|
+
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
123
|
+
await writeFile(join(tempDir, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
124
|
+
|
|
125
|
+
const manifest = {
|
|
126
|
+
$schema: 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json',
|
|
127
|
+
scaffoldVersion: '1.0.0',
|
|
128
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
129
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
130
|
+
projectName: 'test',
|
|
131
|
+
awsRegion: 'eu-north-1',
|
|
132
|
+
enabledModules: ['backend-lambda'],
|
|
133
|
+
files: [
|
|
134
|
+
{ path: '.mc/infra/modules/app.ts', module: 'backend-lambda', moduleVersion: '1.0.0',
|
|
135
|
+
sha256: 'abc', wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'owned' as const }
|
|
136
|
+
]
|
|
137
|
+
};
|
|
138
|
+
await writeFile(join(tempDir, '.tib', 'manifest.json'), JSON.stringify(manifest));
|
|
139
|
+
|
|
140
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
141
|
+
const check = report.checks.find(c => c.name === 'Layout: owned files in .mc/');
|
|
142
|
+
expect(check?.status).toBe('PASS');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('FAIL when owned file is outside .mc/ (old layout)', async () => {
|
|
146
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
147
|
+
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
148
|
+
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
149
|
+
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
150
|
+
|
|
151
|
+
await writeFile(join(tempDir, 'infra', 'modules', 'app.ts'), '// @mc-scaffold: backend-lambda@1.0.0\n');
|
|
152
|
+
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
153
|
+
await writeFile(join(tempDir, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
154
|
+
|
|
155
|
+
const manifest = {
|
|
156
|
+
$schema: 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json',
|
|
157
|
+
scaffoldVersion: '1.0.0',
|
|
158
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
159
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
160
|
+
projectName: 'test',
|
|
161
|
+
awsRegion: 'eu-north-1',
|
|
162
|
+
enabledModules: ['backend-lambda'],
|
|
163
|
+
files: [
|
|
164
|
+
{ path: 'infra/modules/app.ts', module: 'backend-lambda', moduleVersion: '1.0.0',
|
|
165
|
+
sha256: 'abc', wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'owned' as const }
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
await writeFile(join(tempDir, '.tib', 'manifest.json'), JSON.stringify(manifest));
|
|
169
|
+
|
|
170
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
171
|
+
const check = report.checks.find(c => c.name === 'Layout: owned files in .mc/');
|
|
172
|
+
expect(check?.status).toBe('FAIL');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('FAIL when manifest entry points at missing file', async () => {
|
|
176
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
177
|
+
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
178
|
+
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
179
|
+
|
|
180
|
+
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
181
|
+
await writeFile(join(tempDir, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
182
|
+
|
|
183
|
+
const manifest = {
|
|
184
|
+
$schema: 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json',
|
|
185
|
+
scaffoldVersion: '1.0.0',
|
|
186
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
187
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
188
|
+
projectName: 'test',
|
|
189
|
+
awsRegion: 'eu-north-1',
|
|
190
|
+
enabledModules: ['backend-lambda'],
|
|
191
|
+
files: [
|
|
192
|
+
{ path: '.mc/infra/missing-file.ts', module: 'backend-lambda', moduleVersion: '1.0.0',
|
|
193
|
+
sha256: 'abc', wasTemplate: false, installedAt: '2026-01-01T00:00:00Z', policy: 'owned' as const }
|
|
194
|
+
]
|
|
195
|
+
};
|
|
196
|
+
await writeFile(join(tempDir, '.tib', 'manifest.json'), JSON.stringify(manifest));
|
|
197
|
+
|
|
198
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
199
|
+
const check = report.checks.find(c => c.name === 'Layout: manifest entries exist on disk');
|
|
200
|
+
expect(check?.status).toBe('FAIL');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('--relocate flag', () => {
|
|
205
|
+
it('moves scaffold-owned files from old layout to .mc/', async () => {
|
|
206
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
207
|
+
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
208
|
+
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
209
|
+
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
210
|
+
|
|
211
|
+
const appContent = '// @mc-scaffold: backend-lambda@1.0.0\nimport * as cdk from "aws-cdk-lib";\n';
|
|
212
|
+
await writeFile(join(tempDir, 'infra', 'modules', 'app.ts'), appContent);
|
|
213
|
+
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
214
|
+
await writeFile(join(tempDir, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
215
|
+
|
|
216
|
+
const { computeChecksumString } = await import('../utils/checksum.js');
|
|
217
|
+
const sha256 = computeChecksumString(appContent);
|
|
218
|
+
|
|
219
|
+
const manifest = {
|
|
220
|
+
$schema: 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json',
|
|
221
|
+
scaffoldVersion: '1.0.0',
|
|
222
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
223
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
224
|
+
projectName: 'test',
|
|
225
|
+
awsRegion: 'eu-north-1',
|
|
226
|
+
enabledModules: ['backend-lambda'],
|
|
227
|
+
files: [
|
|
228
|
+
{ path: 'infra/modules/app.ts', module: 'backend-lambda', moduleVersion: '1.0.0',
|
|
229
|
+
sha256, wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'owned' as const }
|
|
230
|
+
]
|
|
231
|
+
};
|
|
232
|
+
await writeFile(join(tempDir, '.tib', 'manifest.json'), JSON.stringify(manifest));
|
|
233
|
+
|
|
234
|
+
const report = await runDoctor({ projectRoot: tempDir, relocate: true });
|
|
235
|
+
|
|
236
|
+
// File moved
|
|
237
|
+
const { existsSync } = await import('node:fs');
|
|
238
|
+
expect(existsSync(join(tempDir, '.tib', 'infra', 'modules', 'app.ts'))).toBe(true);
|
|
239
|
+
expect(existsSync(join(tempDir, 'infra', 'modules', 'app.ts'))).toBe(false);
|
|
240
|
+
|
|
241
|
+
// Summary pass
|
|
242
|
+
const summary = report.checks.find(c => c.name === 'Relocate: summary');
|
|
243
|
+
expect(summary?.status).toMatch(/PASS|WARN/);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('leaves hand-edited files in place with warning on --relocate', async () => {
|
|
247
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
248
|
+
await mkdir(join(tempDir, 'infra', 'modules'), { recursive: true });
|
|
249
|
+
|
|
250
|
+
const originalContent = '// @mc-scaffold: backend-lambda@1.0.0\noriginal\n';
|
|
251
|
+
const modifiedContent = '// @mc-scaffold: backend-lambda@1.0.0\nmodified by user\n';
|
|
252
|
+
await writeFile(join(tempDir, 'infra', 'modules', 'app.ts'), modifiedContent);
|
|
253
|
+
await writeFile(join(tempDir, '.tib', 'scaffold-config.json'), JSON.stringify({ domainIds: [] }));
|
|
254
|
+
|
|
255
|
+
const { computeChecksumString } = await import('../utils/checksum.js');
|
|
256
|
+
const originalSha = computeChecksumString(originalContent); // manifest has original sha
|
|
257
|
+
|
|
258
|
+
const manifest = {
|
|
259
|
+
$schema: 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json',
|
|
260
|
+
scaffoldVersion: '1.0.0',
|
|
261
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
262
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
263
|
+
projectName: 'test',
|
|
264
|
+
awsRegion: 'eu-north-1',
|
|
265
|
+
enabledModules: ['backend-lambda'],
|
|
266
|
+
files: [
|
|
267
|
+
{ path: 'infra/modules/app.ts', module: 'backend-lambda', moduleVersion: '1.0.0',
|
|
268
|
+
sha256: originalSha, wasTemplate: true, installedAt: '2026-01-01T00:00:00Z', policy: 'owned' as const }
|
|
269
|
+
]
|
|
270
|
+
};
|
|
271
|
+
await writeFile(join(tempDir, '.tib', 'manifest.json'), JSON.stringify(manifest));
|
|
272
|
+
|
|
273
|
+
await runDoctor({ projectRoot: tempDir, relocate: true });
|
|
274
|
+
|
|
275
|
+
// File left in place
|
|
276
|
+
const { existsSync } = await import('node:fs');
|
|
277
|
+
expect(existsSync(join(tempDir, 'infra', 'modules', 'app.ts'))).toBe(true);
|
|
278
|
+
expect(existsSync(join(tempDir, '.tib', 'infra', 'modules', 'app.ts'))).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('FAIL when no manifest present on --relocate', async () => {
|
|
282
|
+
await mkdir(join(tempDir, '.tib'), { recursive: true });
|
|
283
|
+
|
|
284
|
+
const report = await runDoctor({ projectRoot: tempDir, relocate: true });
|
|
285
|
+
|
|
286
|
+
expect(report.exitCode).toBe(1);
|
|
287
|
+
const check = report.checks.find(c => c.name === 'Relocate: manifest present');
|
|
288
|
+
expect(check?.status).toBe('FAIL');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../runtime-stubs/load-secrets.js', () => ({
|
|
4
|
+
loadLocalSecrets: vi.fn().mockResolvedValue(Object.freeze({ MY_SECRET: 'test-value' })),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('../utils/logger.js', () => ({
|
|
8
|
+
createCliLogger: vi.fn().mockReturnValue({
|
|
9
|
+
info: vi.fn(),
|
|
10
|
+
debug: vi.fn(),
|
|
11
|
+
error: vi.fn(),
|
|
12
|
+
warn: vi.fn(),
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { hydrateLocalCtx } from '../runtime-stubs/hydrate-local-ctx.js';
|
|
17
|
+
|
|
18
|
+
describe('hydrateLocalCtx', () => {
|
|
19
|
+
const DOMAIN_ROOT = '/fake/domain';
|
|
20
|
+
|
|
21
|
+
it('returns a ctx with all 14 DomainContext fields populated', async () => {
|
|
22
|
+
const { ctx } = await hydrateLocalCtx({ domainRoot: DOMAIN_ROOT });
|
|
23
|
+
|
|
24
|
+
expect(ctx.db).toBeDefined();
|
|
25
|
+
expect(ctx.publish).toBeTypeOf('function');
|
|
26
|
+
expect(ctx.actions).toBeDefined();
|
|
27
|
+
expect(ctx.integrations).toBeDefined();
|
|
28
|
+
expect(ctx.jobs).toBeDefined();
|
|
29
|
+
expect(ctx.flows).toBeDefined();
|
|
30
|
+
expect(ctx.idempotency).toBeDefined();
|
|
31
|
+
expect(ctx.cache).toBeDefined();
|
|
32
|
+
expect(ctx.secrets).toBeDefined();
|
|
33
|
+
expect(ctx.actor).toBeDefined();
|
|
34
|
+
expect(ctx.tenant).toBeDefined();
|
|
35
|
+
expect(ctx.logger).toBeDefined();
|
|
36
|
+
expect(ctx.tracer).toBeDefined();
|
|
37
|
+
expect(ctx.fetch).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('uses the provided tenantId', async () => {
|
|
41
|
+
const { ctx } = await hydrateLocalCtx({ domainRoot: DOMAIN_ROOT, tenantId: 'tenant-abc' });
|
|
42
|
+
expect(ctx.tenant.id).toBe('tenant-abc');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('uses the provided actorSub', async () => {
|
|
46
|
+
const { ctx } = await hydrateLocalCtx({ domainRoot: DOMAIN_ROOT, actorSub: 'my-user' });
|
|
47
|
+
expect(ctx.actor.sub).toBe('my-user');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('captures start empty', async () => {
|
|
51
|
+
const { captures } = await hydrateLocalCtx({ domainRoot: DOMAIN_ROOT });
|
|
52
|
+
expect(captures.events).toHaveLength(0);
|
|
53
|
+
expect(captures.jobs).toHaveLength(0);
|
|
54
|
+
expect(captures.executions).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('captures events published via ctx.publish', async () => {
|
|
58
|
+
const { ctx, captures } = await hydrateLocalCtx({ domainRoot: DOMAIN_ROOT });
|
|
59
|
+
await ctx.publish('test.event', { foo: 'bar' });
|
|
60
|
+
expect(captures.events).toHaveLength(1);
|
|
61
|
+
expect(captures.events[0]!.eventType).toBe('test.event');
|
|
62
|
+
expect(captures.events[0]!.data).toEqual({ foo: 'bar' });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('captures jobs enqueued via ctx.jobs.enqueue', async () => {
|
|
66
|
+
const { ctx, captures } = await hydrateLocalCtx({ domainRoot: DOMAIN_ROOT });
|
|
67
|
+
await ctx.jobs.enqueue('send-email', { to: 'a@b.com' });
|
|
68
|
+
expect(captures.jobs).toHaveLength(1);
|
|
69
|
+
expect(captures.jobs[0]!.jobName).toBe('send-email');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { loadDomainModuleConfig } from '../builder/load-domain-module-config.js';
|
|
3
|
+
|
|
4
|
+
describe('loadDomainModuleConfig', () => {
|
|
5
|
+
it('returns default optional config when config file is absent', async () => {
|
|
6
|
+
const result = await loadDomainModuleConfig('/nonexistent-project-root');
|
|
7
|
+
expect(result.mode).toBe('optional');
|
|
8
|
+
expect(result.legacyAllowlist).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
|
|
3
|
+
|
|
4
|
+
vi.mock('../runtime-stubs/hydrate-local-ctx.js', () => ({
|
|
5
|
+
hydrateLocalCtx: vi.fn().mockResolvedValue({
|
|
6
|
+
ctx: {
|
|
7
|
+
db: {}, publish: vi.fn(), actions: {}, integrations: {},
|
|
8
|
+
jobs: { enqueue: vi.fn() }, flows: { start: vi.fn() },
|
|
9
|
+
idempotency: {}, cache: {}, secrets: {}, actor: {}, tenant: {},
|
|
10
|
+
logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
|
|
11
|
+
tracer: {}, fetch: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
captures: { events: [], jobs: [], executions: [] },
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { mountRoutes } from '../server/mount-routes.js';
|
|
18
|
+
|
|
19
|
+
function makeRegistry(apis: Array<Partial<DomainRegistry['apis'][0]>>): DomainRegistry {
|
|
20
|
+
return {
|
|
21
|
+
schemaVersion: '1',
|
|
22
|
+
domainRoot: '/fake/domain',
|
|
23
|
+
domain: { id: 'test', kind: 'domain', name: 'Test', tenancy: 'none' },
|
|
24
|
+
apis: apis.map((a, i) => ({
|
|
25
|
+
id: `api-${i}`,
|
|
26
|
+
kind: 'api' as const,
|
|
27
|
+
handlerFile: `api/handler-${i}.ts`,
|
|
28
|
+
path: `/route-${i}`,
|
|
29
|
+
method: 'GET',
|
|
30
|
+
authType: 'none' as const,
|
|
31
|
+
...a,
|
|
32
|
+
})),
|
|
33
|
+
webhooks: [],
|
|
34
|
+
subscribers: [],
|
|
35
|
+
schedules: [],
|
|
36
|
+
jobs: [],
|
|
37
|
+
actions: [],
|
|
38
|
+
integrations: [],
|
|
39
|
+
events: [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('mountRoutes', () => {
|
|
44
|
+
it('registers one route per api entry', async () => {
|
|
45
|
+
const routes: Array<{ method: unknown; url: string }> = [];
|
|
46
|
+
const server = {
|
|
47
|
+
route: vi.fn((opts: { method: unknown; url: string }) => { routes.push(opts); }),
|
|
48
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
vi.doMock('/fake/domain/api/handler-0.ts', () => ({
|
|
52
|
+
myApi: { id: 'api-0', _kind: 'api', handler: vi.fn().mockResolvedValue({ ok: true }) },
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
const registry = makeRegistry([{ id: 'api-0', handlerFile: 'api/handler-0.ts', path: '/route-0', method: 'GET' }]);
|
|
56
|
+
|
|
57
|
+
await mountRoutes({ server: server as never, registry, domainRoot: '/fake/domain' });
|
|
58
|
+
|
|
59
|
+
expect(server.route).toHaveBeenCalledTimes(1);
|
|
60
|
+
expect(routes[0]!.url).toBe('/route-0');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('converts {param} path syntax to :param for Fastify', async () => {
|
|
64
|
+
const routes: Array<{ url: string }> = [];
|
|
65
|
+
const server = {
|
|
66
|
+
route: vi.fn((opts: { url: string }) => { routes.push(opts); }),
|
|
67
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
vi.doMock('/fake/domain/api/handler-1.ts', () => ({
|
|
71
|
+
myApi: { id: 'api-1', _kind: 'api', handler: vi.fn() },
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
const registry = makeRegistry([{
|
|
75
|
+
id: 'api-1', handlerFile: 'api/handler-1.ts', path: '/users/{id}/orders', method: 'GET',
|
|
76
|
+
}]);
|
|
77
|
+
|
|
78
|
+
await mountRoutes({ server: server as never, registry, domainRoot: '/fake/domain' });
|
|
79
|
+
|
|
80
|
+
expect(routes[0]!.url).toBe('/users/:id/orders');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('skips entries where handler function is not found', async () => {
|
|
84
|
+
const server = {
|
|
85
|
+
route: vi.fn(),
|
|
86
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
vi.doMock('/fake/domain/api/handler-2.ts', () => ({
|
|
90
|
+
noHandler: { id: 'api-2', _kind: 'api' },
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const registry = makeRegistry([{ id: 'api-2', handlerFile: 'api/handler-2.ts', path: '/skip', method: 'GET' }]);
|
|
94
|
+
|
|
95
|
+
await mountRoutes({ server: server as never, registry, domainRoot: '/fake/domain' });
|
|
96
|
+
|
|
97
|
+
expect(server.route).not.toHaveBeenCalled();
|
|
98
|
+
expect(server.log.warn).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { join, resolve, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
const repoRoot = resolve(__dirname, '../../../../..');
|
|
9
|
+
const backendLambdaDir = join(repoRoot, 'scaffold-src', 'modules', 'backend-lambda');
|
|
10
|
+
const coreDir = join(repoRoot, 'scaffold-src', 'modules', 'core');
|
|
11
|
+
|
|
12
|
+
describe('scaffold-src layout (Part A)', () => {
|
|
13
|
+
it('backend-lambda has app.ts.tpl under .mc/infra/modules/', () => {
|
|
14
|
+
expect(existsSync(join(backendLambdaDir, '.mc/infra/modules/app.ts.tpl'))).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('backend-lambda infra/ directory is removed', () => {
|
|
18
|
+
expect(existsSync(join(backendLambdaDir, 'infra'))).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('backend-lambda has root cdk.json.tpl seed', () => {
|
|
22
|
+
expect(existsSync(join(backendLambdaDir, 'cdk.json.tpl'))).toBe(true);
|
|
23
|
+
const content = readFileSync(join(backendLambdaDir, 'cdk.json.tpl'), 'utf-8');
|
|
24
|
+
const parsed = JSON.parse(content);
|
|
25
|
+
expect(parsed.app).toContain('.mc/infra/modules/app.ts');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('core has .mc/workflows/ci.yml.tpl', () => {
|
|
29
|
+
expect(existsSync(join(coreDir, '.mc/workflows/ci.yml.tpl'))).toBe(true);
|
|
30
|
+
});
|
|
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');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('backend-lambda tarball-manifest.json maps .mc/deploy.yml.tpl to .github/workflows/mc-deploy.yml', () => {
|
|
39
|
+
const tmPath = join(backendLambdaDir, 'tarball-manifest.json');
|
|
40
|
+
expect(existsSync(tmPath)).toBe(true);
|
|
41
|
+
const tm = JSON.parse(readFileSync(tmPath, 'utf-8'));
|
|
42
|
+
const deployOverride = tm.fileOverrides.find((e: { path: string }) => e.path === '.mc/deploy.yml.tpl');
|
|
43
|
+
expect(deployOverride?.installedAs).toBe('.github/workflows/mc-deploy.yml');
|
|
44
|
+
expect(deployOverride?.policy).toBe('owned');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('package-scaffold policy inference', () => {
|
|
4
|
+
function inferPolicyFromPath(filePath: string): string {
|
|
5
|
+
if (/^infra\//.test(filePath) || /^\.tib\/infra\//.test(filePath) ||
|
|
6
|
+
filePath === 'mc-deploy.yml' || /^\.github\/workflows\//.test(filePath)) {
|
|
7
|
+
return 'owned';
|
|
8
|
+
}
|
|
9
|
+
const seedPatterns = [
|
|
10
|
+
/^frontend\/src\/pages\/auth\//,
|
|
11
|
+
/^frontend\/src\/pages\/account\//,
|
|
12
|
+
/^frontend\/src\/pages\/shell\//,
|
|
13
|
+
/^frontend\/src\/pages\/_errors\//,
|
|
14
|
+
/^frontend\/src\/layouts\//,
|
|
15
|
+
/^frontend\/src\/styles\/brand\.css$/,
|
|
16
|
+
];
|
|
17
|
+
if (seedPatterns.some(p => p.test(filePath))) return 'seed';
|
|
18
|
+
return 'tracked';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
it('returns owned for .mc/infra/ paths', () => {
|
|
22
|
+
expect(inferPolicyFromPath('.mc/infra/modules/app.ts')).toBe('owned');
|
|
23
|
+
expect(inferPolicyFromPath('.mc/infra/cdk.json')).toBe('owned');
|
|
24
|
+
expect(inferPolicyFromPath('.mc/infra/tsconfig.json')).toBe('owned');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns owned for .github/workflows/ paths', () => {
|
|
28
|
+
expect(inferPolicyFromPath('.github/workflows/tib-ci.yml')).toBe('owned');
|
|
29
|
+
expect(inferPolicyFromPath('.github/workflows/mc-deploy.yml')).toBe('owned');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns seed for frontend page paths', () => {
|
|
33
|
+
expect(inferPolicyFromPath('frontend/src/pages/auth/login.tsx')).toBe('seed');
|
|
34
|
+
expect(inferPolicyFromPath('frontend/src/styles/brand.css')).toBe('seed');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns tracked for domain files', () => {
|
|
38
|
+
expect(inferPolicyFromPath('domains/payments/api/charge.ts')).toBe('tracked');
|
|
39
|
+
expect(inferPolicyFromPath('packages/domain-cli/src/utils/manifest.ts')).toBe('tracked');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns owned for mc-deploy.yml', () => {
|
|
43
|
+
expect(inferPolicyFromPath('mc-deploy.yml')).toBe('owned');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { writeFile, unlink } from 'fs/promises';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { computeChecksumBuffer, computeChecksumString, computeChecksumFile, isDrifted } from '../../utils/checksum.js';
|
|
6
|
+
|
|
7
|
+
describe('computeChecksumBuffer', () => {
|
|
8
|
+
it('returns lowercase hex sha256', () => {
|
|
9
|
+
const buf = Buffer.from('hello world', 'utf-8');
|
|
10
|
+
const result = computeChecksumBuffer(buf);
|
|
11
|
+
expect(result).toMatch(/^[0-9a-f]{64}$/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('is deterministic for same input', () => {
|
|
15
|
+
const buf = Buffer.from('test content', 'utf-8');
|
|
16
|
+
expect(computeChecksumBuffer(buf)).toBe(computeChecksumBuffer(buf));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('differs for different input', () => {
|
|
20
|
+
const a = computeChecksumBuffer(Buffer.from('aaa'));
|
|
21
|
+
const b = computeChecksumBuffer(Buffer.from('bbb'));
|
|
22
|
+
expect(a).not.toBe(b);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('matches known SHA-256 value', () => {
|
|
26
|
+
// echo -n "hello" | sha256sum = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
|
|
27
|
+
const buf = Buffer.from('hello', 'utf-8');
|
|
28
|
+
expect(computeChecksumBuffer(buf)).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('computeChecksumString', () => {
|
|
33
|
+
it('returns same result as buffer version for UTF-8 string', () => {
|
|
34
|
+
const content = 'test string content';
|
|
35
|
+
const fromStr = computeChecksumString(content);
|
|
36
|
+
const fromBuf = computeChecksumBuffer(Buffer.from(content, 'utf-8'));
|
|
37
|
+
expect(fromStr).toBe(fromBuf);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('computeChecksumFile', () => {
|
|
42
|
+
it('returns correct checksum for a file', async () => {
|
|
43
|
+
const tmpFile = join(tmpdir(), `checksum-test-${Date.now()}.txt`);
|
|
44
|
+
const content = 'file content for checksum';
|
|
45
|
+
await writeFile(tmpFile, content, 'utf-8');
|
|
46
|
+
try {
|
|
47
|
+
const result = await computeChecksumFile(tmpFile);
|
|
48
|
+
const expected = computeChecksumString(content);
|
|
49
|
+
expect(result).toBe(expected);
|
|
50
|
+
} finally {
|
|
51
|
+
await unlink(tmpFile).catch(() => undefined);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns null for non-existent file', async () => {
|
|
56
|
+
const result = await computeChecksumFile('/non/existent/path/file.txt');
|
|
57
|
+
expect(result).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('isDrifted', () => {
|
|
62
|
+
it('returns false when file matches expected checksum', async () => {
|
|
63
|
+
const tmpFile = join(tmpdir(), `drift-test-${Date.now()}.txt`);
|
|
64
|
+
const content = 'unchanged content';
|
|
65
|
+
await writeFile(tmpFile, content, 'utf-8');
|
|
66
|
+
const expected = computeChecksumString(content);
|
|
67
|
+
try {
|
|
68
|
+
const result = await isDrifted(tmpFile, expected);
|
|
69
|
+
expect(result).toBe(false);
|
|
70
|
+
} finally {
|
|
71
|
+
await unlink(tmpFile).catch(() => undefined);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns true when file has been modified', async () => {
|
|
76
|
+
const tmpFile = join(tmpdir(), `drift-test-${Date.now()}.txt`);
|
|
77
|
+
await writeFile(tmpFile, 'modified content', 'utf-8');
|
|
78
|
+
try {
|
|
79
|
+
const result = await isDrifted(tmpFile, 'deadbeef'.repeat(8));
|
|
80
|
+
expect(result).toBe(true);
|
|
81
|
+
} finally {
|
|
82
|
+
await unlink(tmpFile).catch(() => undefined);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns null when file does not exist', async () => {
|
|
87
|
+
const result = await isDrifted('/non/existent/file.txt', 'abc123');
|
|
88
|
+
expect(result).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|