@monster-orange/den 0.0.0 → 0.1.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 +67 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- package/bin/den +61 -0
- package/docs/distributions.md +256 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +99 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +406 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +566 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +178 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Module Scanner
|
|
3
|
+
* Auto-discovers test modules from filesystem
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { listReposWithManifests } from "../manifest/workspace-manifests.js";
|
|
7
|
+
import * as fs from "fs/promises";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import * as yaml from "yaml";
|
|
10
|
+
import { discoverRepos } from "../../pillars/smoke/discover.js";
|
|
11
|
+
import { discoverJourneySpecs } from "../../pillars/journeys/discover.js";
|
|
12
|
+
import {
|
|
13
|
+
discoverSecurityTargets,
|
|
14
|
+
securityTargetId,
|
|
15
|
+
} from "../../pillars/security/discover.js";
|
|
16
|
+
import { TEST_FILE_EXTENSIONS, SKIP_TEST_DIRS } from "../constants.js";
|
|
17
|
+
import { getDistRoot } from "../paths.js";
|
|
18
|
+
import { enginePath } from "../engine.js";
|
|
19
|
+
import { getPerfTestsRoot } from "../perf/test-paths.js";
|
|
20
|
+
import type {
|
|
21
|
+
PerformanceModuleConfig,
|
|
22
|
+
PerfOptionalField,
|
|
23
|
+
Pillar,
|
|
24
|
+
} from "../types/index.js";
|
|
25
|
+
|
|
26
|
+
export interface ModuleManifest {
|
|
27
|
+
name?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
tags?: string[];
|
|
31
|
+
/** Declarative launch-time options for perf tests (rendered by the web UI). */
|
|
32
|
+
optionalFields?: PerfOptionalField[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TestModule {
|
|
36
|
+
name: string;
|
|
37
|
+
displayName: string;
|
|
38
|
+
description: string;
|
|
39
|
+
icon: string;
|
|
40
|
+
path: string;
|
|
41
|
+
testCount: number;
|
|
42
|
+
tags: string[];
|
|
43
|
+
/** Declarative launch-time options for perf tests; empty for non-perf modules. */
|
|
44
|
+
optionalFields?: PerfOptionalField[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ModuleScanner {
|
|
48
|
+
private rootDir: string;
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
// Den-root anchored (read-only discovery): a cwd default made `den`
|
|
52
|
+
// invoked outside the repo silently report zero test modules.
|
|
53
|
+
this.rootDir = getDistRoot();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Scan for test modules in a given pillar
|
|
58
|
+
*/
|
|
59
|
+
async scanModules(pillar: Pillar): Promise<TestModule[]> {
|
|
60
|
+
if (pillar === "journeys") {
|
|
61
|
+
return this.scanJourneyModules();
|
|
62
|
+
}
|
|
63
|
+
if (pillar === "security") {
|
|
64
|
+
return this.scanSecurityTargets();
|
|
65
|
+
}
|
|
66
|
+
if (pillar === "smoke") {
|
|
67
|
+
return this.scanSmokeServices();
|
|
68
|
+
}
|
|
69
|
+
if (pillar === "performance") {
|
|
70
|
+
return this.scanPerformanceModules();
|
|
71
|
+
}
|
|
72
|
+
if (pillar === "synthetic") {
|
|
73
|
+
return this.scanSyntheticModules();
|
|
74
|
+
}
|
|
75
|
+
if (pillar === "suites") {
|
|
76
|
+
return this.scanRepoSuites();
|
|
77
|
+
}
|
|
78
|
+
return this.scanFilesystemModules(
|
|
79
|
+
enginePath("src", "pillars", "api", "tests"),
|
|
80
|
+
pillar,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Suites repos declare under `tests:` in their manifest. */
|
|
85
|
+
private scanRepoSuites(): TestModule[] {
|
|
86
|
+
return listReposWithManifests().flatMap((r) =>
|
|
87
|
+
Object.entries(r.manifest.tests ?? {}).map(([name, suite]) => ({
|
|
88
|
+
name: `${r.repo}/${name}`,
|
|
89
|
+
displayName: `${r.repo}/${suite.label ?? name}`,
|
|
90
|
+
description: suite.cmd,
|
|
91
|
+
icon: "🧪",
|
|
92
|
+
path: r.repoPath,
|
|
93
|
+
testCount: 1,
|
|
94
|
+
tags: r.org ? [r.org] : [],
|
|
95
|
+
})),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if directory should be skipped
|
|
101
|
+
*/
|
|
102
|
+
private shouldSkipDirectory(dirName: string): boolean {
|
|
103
|
+
// Skip directories starting with underscore
|
|
104
|
+
if (dirName.startsWith("_")) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (SKIP_TEST_DIRS as readonly string[]).includes(dirName);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Count test files in directory
|
|
113
|
+
*/
|
|
114
|
+
/**
|
|
115
|
+
* Scan registered repos (via discoverRepos) and return their smoke services as modules.
|
|
116
|
+
* Reads from both config/local/registered-repos.json and .den/ symlinks.
|
|
117
|
+
*/
|
|
118
|
+
private async scanSmokeServices(): Promise<TestModule[]> {
|
|
119
|
+
const modules: TestModule[] = [];
|
|
120
|
+
|
|
121
|
+
const repos = discoverRepos();
|
|
122
|
+
for (const repo of repos) {
|
|
123
|
+
for (const [svcName, svc] of Object.entries(repo.manifest.smoke ?? {})) {
|
|
124
|
+
const testDir = path.resolve(repo.repoPath, svc.testDir);
|
|
125
|
+
const testCount = await this.countSmokeFiles(testDir);
|
|
126
|
+
modules.push({
|
|
127
|
+
name: `${repo.repoName}/${svcName}`,
|
|
128
|
+
displayName: `${repo.repoName} — ${svcName}`,
|
|
129
|
+
description: `${svcName} smoke tests from ${repo.repoName}`,
|
|
130
|
+
icon: svcName === "api" ? "🔗" : "🌐",
|
|
131
|
+
path: testDir,
|
|
132
|
+
testCount,
|
|
133
|
+
tags: [svcName],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return modules;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* List declared security targets as modules — manifest-driven via
|
|
143
|
+
* discoverSecurityTargets(), named "<repo>/<service>" like smoke. A
|
|
144
|
+
* failing discovery degrades to a warned empty list (interactive picker);
|
|
145
|
+
* the run entrypoint keeps the loud fail-fast posture.
|
|
146
|
+
*/
|
|
147
|
+
private async scanSecurityTargets(): Promise<TestModule[]> {
|
|
148
|
+
try {
|
|
149
|
+
return discoverSecurityTargets().map((target) => ({
|
|
150
|
+
name: securityTargetId(target),
|
|
151
|
+
displayName: `${target.repoName} — ${target.serviceName}`,
|
|
152
|
+
description: `ZAP scan target from ${target.repoName}`,
|
|
153
|
+
icon: "🛡️",
|
|
154
|
+
path: target.repoPath,
|
|
155
|
+
testCount: 1,
|
|
156
|
+
tags: [target.config.baseUrlKey ?? "hosts"],
|
|
157
|
+
}));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.warn(
|
|
160
|
+
"Warning: Could not scan security modules:",
|
|
161
|
+
(error as Error).message,
|
|
162
|
+
);
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* List discovered journey specs as modules — manifest-driven via
|
|
169
|
+
* discoverJourneySpecs() (one module per journey spec, named
|
|
170
|
+
* "<repo>/<journey-name>" like smoke's "<repo>/<service>"). No repos
|
|
171
|
+
* registered naturally yields an empty list; a failing discovery
|
|
172
|
+
* (missing testDir, invalid spec) degrades to a warned empty list here
|
|
173
|
+
* because this scanner feeds interactive pickers — the run entrypoint
|
|
174
|
+
* keeps the loud fail-fast posture.
|
|
175
|
+
*/
|
|
176
|
+
private async scanJourneyModules(): Promise<TestModule[]> {
|
|
177
|
+
try {
|
|
178
|
+
return discoverJourneySpecs().map((discovered) => ({
|
|
179
|
+
name: `${discovered.repoName}/${discovered.spec.name}`,
|
|
180
|
+
displayName: `${discovered.repoName} — ${discovered.spec.name}`,
|
|
181
|
+
description: `${discovered.spec.surface} journey from ${discovered.repoName}`,
|
|
182
|
+
icon: "🧭",
|
|
183
|
+
path: discovered.specPath,
|
|
184
|
+
testCount: 1,
|
|
185
|
+
tags: [discovered.spec.surface],
|
|
186
|
+
}));
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.warn(
|
|
189
|
+
"Warning: Could not scan journeys modules:",
|
|
190
|
+
(error as Error).message,
|
|
191
|
+
);
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async scanPerformanceModules(): Promise<TestModule[]> {
|
|
197
|
+
// The distribution's own suites (the engine ships none; a distribution
|
|
198
|
+
// without any has no directory, which is not an error), then the suites
|
|
199
|
+
// registered repos declare in their manifests.
|
|
200
|
+
const distTestsDir = getPerfTestsRoot(this.rootDir);
|
|
201
|
+
const modules = (await this.pathExists(distTestsDir))
|
|
202
|
+
? await this.scanFilesystemModules(distTestsDir, "performance")
|
|
203
|
+
: [];
|
|
204
|
+
|
|
205
|
+
for (const repo of discoverRepos()) {
|
|
206
|
+
for (const [moduleName, config] of Object.entries(
|
|
207
|
+
repo.manifest.performance ?? {},
|
|
208
|
+
)) {
|
|
209
|
+
const modulePath = path.resolve(repo.repoPath, config.testDir);
|
|
210
|
+
const manifest = await this.loadManifest(modulePath);
|
|
211
|
+
|
|
212
|
+
modules.push({
|
|
213
|
+
name: `${repo.repoName}/${moduleName}`,
|
|
214
|
+
displayName: manifest.name || `${repo.repoName} — ${moduleName}`,
|
|
215
|
+
description:
|
|
216
|
+
manifest.description ||
|
|
217
|
+
`${moduleName} performance tests from ${repo.repoName}`,
|
|
218
|
+
icon: manifest.icon || this.getModuleIcon(moduleName),
|
|
219
|
+
path: modulePath,
|
|
220
|
+
testCount: await this.countPerformanceFiles(modulePath, config),
|
|
221
|
+
tags: manifest.tags || config.tags || [],
|
|
222
|
+
optionalFields:
|
|
223
|
+
manifest.optionalFields ?? config.optionalFields ?? [],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
modules.sort((a, b) => a.name.localeCompare(b.name));
|
|
229
|
+
return modules;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Scan discovered repos for synthetic test modules.
|
|
234
|
+
* Uses workspace-aware discoverRepos() (active workspace when Local Dev is
|
|
235
|
+
* enabled, registered-repos.json + .den/ otherwise).
|
|
236
|
+
* Falls back to pillars/browser/tests/ if no repo provides synthetics config.
|
|
237
|
+
*/
|
|
238
|
+
private async scanSyntheticModules(): Promise<TestModule[]> {
|
|
239
|
+
const modules: TestModule[] = [];
|
|
240
|
+
let foundAny = false;
|
|
241
|
+
|
|
242
|
+
const repos = discoverRepos();
|
|
243
|
+
for (const repo of repos) {
|
|
244
|
+
const manifest = repo.manifest as {
|
|
245
|
+
synthetics?: { testDir: string };
|
|
246
|
+
} | null;
|
|
247
|
+
if (!manifest?.synthetics?.testDir) continue;
|
|
248
|
+
|
|
249
|
+
const testDir = path.resolve(repo.repoPath, manifest.synthetics.testDir);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const entries = await fs.readdir(testDir, { withFileTypes: true });
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
if (!entry.isDirectory()) continue;
|
|
255
|
+
if (this.shouldSkipDirectory(entry.name)) continue;
|
|
256
|
+
|
|
257
|
+
const modulePath = path.join(testDir, entry.name);
|
|
258
|
+
const testCount = await this.countTestFiles(modulePath, "synthetic");
|
|
259
|
+
const modManifest = await this.loadManifest(modulePath);
|
|
260
|
+
|
|
261
|
+
modules.push({
|
|
262
|
+
name: entry.name,
|
|
263
|
+
displayName: modManifest.name || this.formatDisplayName(entry.name),
|
|
264
|
+
description:
|
|
265
|
+
modManifest.description ||
|
|
266
|
+
(await this.getModuleDescription(modulePath)),
|
|
267
|
+
icon: modManifest.icon || this.getModuleIcon(entry.name),
|
|
268
|
+
path: modulePath,
|
|
269
|
+
testCount,
|
|
270
|
+
tags: modManifest.tags || [],
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
foundAny = true;
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
// testDir doesn't exist or isn't readable — skip
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Fall back to synthetic/tests/ if no discovered repos provide synthetics config
|
|
281
|
+
if (!foundAny) {
|
|
282
|
+
// Distribution-owned (Den ships no browser tests of its own).
|
|
283
|
+
const testsDir = path.join(
|
|
284
|
+
this.rootDir,
|
|
285
|
+
"src",
|
|
286
|
+
"pillars",
|
|
287
|
+
"browser",
|
|
288
|
+
"tests",
|
|
289
|
+
);
|
|
290
|
+
try {
|
|
291
|
+
const entries = await fs.readdir(testsDir, { withFileTypes: true });
|
|
292
|
+
for (const entry of entries) {
|
|
293
|
+
if (!entry.isDirectory()) continue;
|
|
294
|
+
if (this.shouldSkipDirectory(entry.name)) continue;
|
|
295
|
+
|
|
296
|
+
const modulePath = path.join(testsDir, entry.name);
|
|
297
|
+
const testCount = await this.countTestFiles(modulePath, "synthetic");
|
|
298
|
+
const manifest = await this.loadManifest(modulePath);
|
|
299
|
+
|
|
300
|
+
modules.push({
|
|
301
|
+
name: entry.name,
|
|
302
|
+
displayName: manifest.name || this.formatDisplayName(entry.name),
|
|
303
|
+
description:
|
|
304
|
+
manifest.description ||
|
|
305
|
+
(await this.getModuleDescription(modulePath)),
|
|
306
|
+
icon: manifest.icon || this.getModuleIcon(entry.name),
|
|
307
|
+
path: modulePath,
|
|
308
|
+
testCount,
|
|
309
|
+
tags: manifest.tags || [],
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
// synthetic/tests/ doesn't exist
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
modules.sort((a, b) => a.name.localeCompare(b.name));
|
|
318
|
+
return modules;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Recursively count .smoke.ts files under a directory
|
|
323
|
+
*/
|
|
324
|
+
private async countSmokeFiles(dirPath: string): Promise<number> {
|
|
325
|
+
let count = 0;
|
|
326
|
+
try {
|
|
327
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
if (entry.isDirectory()) {
|
|
330
|
+
count += await this.countSmokeFiles(path.join(dirPath, entry.name));
|
|
331
|
+
} else if (
|
|
332
|
+
TEST_FILE_EXTENSIONS.smoke.some((ext) => entry.name.endsWith(ext))
|
|
333
|
+
) {
|
|
334
|
+
count++;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch {
|
|
338
|
+
// directory unreadable (e.g. symlink not yet linked)
|
|
339
|
+
}
|
|
340
|
+
return count;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private async scanFilesystemModules(
|
|
344
|
+
testsDir: string,
|
|
345
|
+
pillar: Pillar,
|
|
346
|
+
): Promise<TestModule[]> {
|
|
347
|
+
try {
|
|
348
|
+
const entries = await fs.readdir(testsDir, { withFileTypes: true });
|
|
349
|
+
const modules: TestModule[] = [];
|
|
350
|
+
|
|
351
|
+
for (const entry of entries) {
|
|
352
|
+
if (!entry.isDirectory()) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (this.shouldSkipDirectory(entry.name)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const modulePath = path.join(testsDir, entry.name);
|
|
360
|
+
const testCount = await this.countTestFiles(modulePath, pillar);
|
|
361
|
+
const manifest = await this.loadManifest(modulePath);
|
|
362
|
+
|
|
363
|
+
modules.push({
|
|
364
|
+
name: entry.name,
|
|
365
|
+
displayName: manifest.name || this.formatDisplayName(entry.name),
|
|
366
|
+
description:
|
|
367
|
+
manifest.description ||
|
|
368
|
+
(await this.getModuleDescription(modulePath)),
|
|
369
|
+
icon: manifest.icon || this.getModuleIcon(entry.name),
|
|
370
|
+
path: modulePath,
|
|
371
|
+
testCount,
|
|
372
|
+
tags: manifest.tags || [],
|
|
373
|
+
optionalFields:
|
|
374
|
+
pillar === "performance" ? (manifest.optionalFields ?? []) : [],
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
modules.sort((a, b) => a.name.localeCompare(b.name));
|
|
379
|
+
return modules;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.warn(
|
|
382
|
+
`Warning: Could not scan ${pillar} test modules:`,
|
|
383
|
+
(error as Error).message,
|
|
384
|
+
);
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private async pathExists(p: string): Promise<boolean> {
|
|
390
|
+
try {
|
|
391
|
+
await fs.access(p);
|
|
392
|
+
return true;
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private async countTestFiles(
|
|
399
|
+
dirPath: string,
|
|
400
|
+
pillar: Pillar,
|
|
401
|
+
): Promise<number> {
|
|
402
|
+
try {
|
|
403
|
+
const files = await fs.readdir(dirPath);
|
|
404
|
+
const exts =
|
|
405
|
+
TEST_FILE_EXTENSIONS[pillar] ?? TEST_FILE_EXTENSIONS.synthetic;
|
|
406
|
+
return files.filter((file) => exts.some((ext) => file.endsWith(ext)))
|
|
407
|
+
.length;
|
|
408
|
+
} catch {
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private async countPerformanceFiles(
|
|
414
|
+
dirPath: string,
|
|
415
|
+
config: PerformanceModuleConfig,
|
|
416
|
+
): Promise<number> {
|
|
417
|
+
if (typeof config.testMatch !== "string" || config.testMatch.length === 0) {
|
|
418
|
+
// One module's bad testMatch must not abort discovery of every other
|
|
419
|
+
// perf module — treat it as "no files found" and move on.
|
|
420
|
+
console.warn(`Invalid performance testMatch for ${dirPath} — skipping`);
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
return await this.countPerformanceFilesFromRoot(dirPath, config, "");
|
|
426
|
+
} catch {
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private async countPerformanceFilesFromRoot(
|
|
432
|
+
dirPath: string,
|
|
433
|
+
config: PerformanceModuleConfig,
|
|
434
|
+
relativeDir: string,
|
|
435
|
+
): Promise<number> {
|
|
436
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
437
|
+
let count = 0;
|
|
438
|
+
|
|
439
|
+
for (const entry of entries) {
|
|
440
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
441
|
+
const relativePath = relativeDir
|
|
442
|
+
? path.join(relativeDir, entry.name)
|
|
443
|
+
: entry.name;
|
|
444
|
+
if (entry.isDirectory()) {
|
|
445
|
+
count += await this.countPerformanceFilesFromRoot(
|
|
446
|
+
entryPath,
|
|
447
|
+
config,
|
|
448
|
+
relativePath,
|
|
449
|
+
);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (this.matchesPerformanceTestFile(relativePath, config.testMatch)) {
|
|
454
|
+
count += 1;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return count;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private matchesPerformanceTestFile(
|
|
462
|
+
relativePath: string,
|
|
463
|
+
testMatch: string,
|
|
464
|
+
): boolean {
|
|
465
|
+
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
466
|
+
|
|
467
|
+
if (normalizedPath === testMatch) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (testMatch.startsWith("**/")) {
|
|
472
|
+
const nestedPattern = testMatch.slice(3);
|
|
473
|
+
if (nestedPattern.startsWith("*.")) {
|
|
474
|
+
return normalizedPath.endsWith(nestedPattern.slice(1));
|
|
475
|
+
}
|
|
476
|
+
return (
|
|
477
|
+
normalizedPath === nestedPattern ||
|
|
478
|
+
normalizedPath.endsWith(`/${nestedPattern}`)
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (testMatch.startsWith("**/*.")) {
|
|
483
|
+
return normalizedPath.endsWith(testMatch.slice("**/*".length));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (!testMatch.includes("/")) {
|
|
487
|
+
return path.basename(normalizedPath) === testMatch;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Format directory name for display
|
|
495
|
+
*/
|
|
496
|
+
private formatDisplayName(dirName: string): string {
|
|
497
|
+
// Convert kebab-case or snake_case to Title Case
|
|
498
|
+
return dirName
|
|
499
|
+
.split(/[-_]/)
|
|
500
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
501
|
+
.join(" ");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Load manifest.yml from module directory
|
|
506
|
+
*/
|
|
507
|
+
private async loadManifest(modulePath: string): Promise<ModuleManifest> {
|
|
508
|
+
const manifestPath = path.join(modulePath, "manifest.yml");
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const content = await fs.readFile(manifestPath, "utf-8");
|
|
512
|
+
const manifest = yaml.parse(content) as ModuleManifest;
|
|
513
|
+
return manifest || {};
|
|
514
|
+
} catch {
|
|
515
|
+
// No manifest or parse error - return empty object
|
|
516
|
+
return {};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Get module description from README or default
|
|
522
|
+
*/
|
|
523
|
+
private async getModuleDescription(modulePath: string): Promise<string> {
|
|
524
|
+
// Try to read description from .gitkeep or README
|
|
525
|
+
const readmePath = path.join(modulePath, ".gitkeep");
|
|
526
|
+
try {
|
|
527
|
+
const content = await fs.readFile(readmePath, "utf-8");
|
|
528
|
+
const firstLine = content.split("\n")[0];
|
|
529
|
+
// Remove markdown heading markers
|
|
530
|
+
return firstLine.replace(/^#\s*/, "").trim();
|
|
531
|
+
} catch {
|
|
532
|
+
// Default descriptions based on common module names
|
|
533
|
+
return this.getDefaultDescription(path.basename(modulePath));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Get default description for common module names
|
|
539
|
+
*/
|
|
540
|
+
private getDefaultDescription(moduleName: string): string {
|
|
541
|
+
const descriptions: Record<string, string> = {
|
|
542
|
+
auth: "Authentication & sessions",
|
|
543
|
+
basic: "Health checks & smoke tests",
|
|
544
|
+
health: "API health checks",
|
|
545
|
+
users: "User API endpoints",
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
return (
|
|
549
|
+
descriptions[moduleName] || `${this.formatDisplayName(moduleName)} tests`
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Get emoji icon for module
|
|
555
|
+
*/
|
|
556
|
+
getModuleIcon(moduleName: string): string {
|
|
557
|
+
const icons: Record<string, string> = {
|
|
558
|
+
basic: "🔍",
|
|
559
|
+
auth: "🔐",
|
|
560
|
+
health: "🔍",
|
|
561
|
+
users: "👥",
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
return icons[moduleName] || "📦";
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Export singleton
|
|
569
|
+
export const moduleScanner = new ModuleScanner();
|