@monster-orange/den 0.0.0-stage → 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 +166 -2
- 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 +104 -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,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-agnostic developer-secrets setup.
|
|
3
|
+
*
|
|
4
|
+
* Dispatches to the strategy selected by `secrets.provider` in
|
|
5
|
+
* config/den.yml (override: DEN_SECRETS_PROVIDER or
|
|
6
|
+
* config/local/user.yml). See src/shared/secrets.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* make den-setup-secrets — interactive setup
|
|
10
|
+
* npx tsx scripts/setup-secrets.ts --check — non-interactive; exit 0 when ready
|
|
11
|
+
* npx tsx scripts/setup-secrets.ts --cli — print "<provider> <binary> <formula>"
|
|
12
|
+
* npx tsx scripts/setup-secrets.ts --gate — like --check, but a backend that
|
|
13
|
+
* is not ready only warns unless
|
|
14
|
+
* secrets.required is true
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from "child_process";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
import chalk from "chalk";
|
|
20
|
+
import {
|
|
21
|
+
getSecretsProvider,
|
|
22
|
+
readSecretsConfig,
|
|
23
|
+
} from "../src/shared/secrets/index.js";
|
|
24
|
+
|
|
25
|
+
import { envKey, product } from "../src/shared/product.js";
|
|
26
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
|
+
const check = process.argv.includes("--check");
|
|
28
|
+
const gate = process.argv.includes("--gate");
|
|
29
|
+
|
|
30
|
+
function run(cmd: string, args: string[]): number {
|
|
31
|
+
return spawnSync(cmd, args, { stdio: "inherit", cwd: ROOT }).status ?? 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function installed(cmd: string): boolean {
|
|
35
|
+
return !spawnSync(cmd, ["--version"], { stdio: "ignore" }).error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const INSTALL: Record<string, string> = {
|
|
39
|
+
doppler: "brew install dopplerhq/cli/doppler",
|
|
40
|
+
op: "brew install 1password-cli",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function otherBackends(current: string): string {
|
|
44
|
+
return (
|
|
45
|
+
"Or pick another backend for just you (config/local/user.yml):\n" +
|
|
46
|
+
" secrets:\n" +
|
|
47
|
+
` provider: env # or 1password / doppler (team default: ${current})\n` +
|
|
48
|
+
` or for one shell: ${envKey("SECRETS_PROVIDER")}=env make web`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main(): Promise<number> {
|
|
53
|
+
const provider = getSecretsProvider();
|
|
54
|
+
const cfg = readSecretsConfig();
|
|
55
|
+
const label = `secrets backend: ${provider.name}`;
|
|
56
|
+
|
|
57
|
+
// `--cli`: print "<provider> <binary> <brew formula>" for make setup's
|
|
58
|
+
// install step ("env -" when the backend needs no CLI).
|
|
59
|
+
if (process.argv.includes("--cli")) {
|
|
60
|
+
const bin =
|
|
61
|
+
provider.name === "doppler"
|
|
62
|
+
? "doppler"
|
|
63
|
+
: provider.name === "1password"
|
|
64
|
+
? "op"
|
|
65
|
+
: "";
|
|
66
|
+
const formula = bin ? INSTALL[bin].replace(/^brew install /, "") : "";
|
|
67
|
+
console.log(`${provider.name} ${bin || "-"} ${formula || "-"}`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (check || gate) {
|
|
72
|
+
const r = await provider.preflight();
|
|
73
|
+
if (r.ok) return 0;
|
|
74
|
+
if (gate && !cfg.required) {
|
|
75
|
+
console.error(
|
|
76
|
+
chalk.yellow(
|
|
77
|
+
`⚠️ ${label} is not ready — continuing.\n` +
|
|
78
|
+
" Test logins and the saved sudo password need it; run " +
|
|
79
|
+
`\`make ${product().cli}-setup-secrets\` when you want them.`,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
console.error(chalk.red(`✗ ${label}\n${r.message ?? ""}`));
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(
|
|
89
|
+
chalk.bold(`\n${product().title} developer secrets (${label})\n`),
|
|
90
|
+
);
|
|
91
|
+
switch (provider.name) {
|
|
92
|
+
case "doppler": {
|
|
93
|
+
if (!installed("doppler")) {
|
|
94
|
+
console.error(
|
|
95
|
+
chalk.red("✗ The Doppler CLI is not installed.\n") +
|
|
96
|
+
` Install it: ${INSTALL.doppler}\n then re-run: make ${product().cli}-setup-secrets\n\n ` +
|
|
97
|
+
otherBackends(provider.name),
|
|
98
|
+
);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
if (!(await provider.isReady())) {
|
|
102
|
+
console.log("→ doppler login");
|
|
103
|
+
if (run("doppler", ["login"]) !== 0) return 1;
|
|
104
|
+
}
|
|
105
|
+
const scope: string[] = [];
|
|
106
|
+
if (cfg.doppler?.project) scope.push("--project", cfg.doppler.project);
|
|
107
|
+
if (cfg.doppler?.config) scope.push("--config", cfg.doppler.config);
|
|
108
|
+
console.log(`→ doppler setup ${scope.join(" ")}`.trim());
|
|
109
|
+
if (
|
|
110
|
+
run("doppler", ["setup", "--no-interactive", ...scope]) !== 0 &&
|
|
111
|
+
scope.length === 0
|
|
112
|
+
) {
|
|
113
|
+
run("doppler", ["setup"]);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "1password":
|
|
118
|
+
if (!installed("op")) {
|
|
119
|
+
console.error(
|
|
120
|
+
chalk.red("✗ The 1Password CLI (op) is not installed.\n") +
|
|
121
|
+
` Install it: ${INSTALL.op}\n\n ` +
|
|
122
|
+
otherBackends(provider.name),
|
|
123
|
+
);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
return run(path.join(ROOT, "node_modules", ".bin", "tsx"), [
|
|
127
|
+
path.join(ROOT, "scripts", "setup-1password.ts"),
|
|
128
|
+
]);
|
|
129
|
+
case "env":
|
|
130
|
+
console.log(
|
|
131
|
+
"The env backend reads secrets from the process environment.\n" +
|
|
132
|
+
`Export them yourself (direnv, CI secrets, \`doppler run -- ${product().cli} up\`).`,
|
|
133
|
+
);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
const r = await provider.preflight();
|
|
137
|
+
console.log(
|
|
138
|
+
r.ok ? chalk.green(`✓ ${label} ready`) : chalk.red(`✗ ${r.message}`),
|
|
139
|
+
);
|
|
140
|
+
return r.ok ? 0 : 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
main().then(
|
|
144
|
+
(code) => process.exit(code),
|
|
145
|
+
(err: unknown) => {
|
|
146
|
+
console.error(err instanceof Error ? err.message : err);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* View JSONL test results
|
|
4
|
+
* Shows summary, trends, and flaky tests
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const RESULTS_DIR = path.join(__dirname, "../results");
|
|
13
|
+
|
|
14
|
+
// Read all JSONL files
|
|
15
|
+
function readResults(pillar, environment, days = 7) {
|
|
16
|
+
const filename = path.join(RESULTS_DIR, `${pillar}-${environment}.jsonl`);
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(filename)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const content = fs.readFileSync(filename, "utf8");
|
|
23
|
+
const lines = content
|
|
24
|
+
.trim()
|
|
25
|
+
.split("\n")
|
|
26
|
+
.filter((line) => line);
|
|
27
|
+
|
|
28
|
+
const cutoffDate = new Date();
|
|
29
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
30
|
+
|
|
31
|
+
return lines
|
|
32
|
+
.map((line) => JSON.parse(line))
|
|
33
|
+
.filter((result) => new Date(result.timestamp) > cutoffDate);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Calculate summary stats
|
|
37
|
+
function calculateStats(results) {
|
|
38
|
+
const total = results.length;
|
|
39
|
+
const passed = results.filter((r) => r.status === "passed").length;
|
|
40
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
41
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
42
|
+
|
|
43
|
+
const avgDuration = results.reduce((sum, r) => sum + r.duration, 0) / total;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
total,
|
|
47
|
+
passed,
|
|
48
|
+
failed,
|
|
49
|
+
skipped,
|
|
50
|
+
avgDuration,
|
|
51
|
+
passRate: ((passed / total) * 100).toFixed(1),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Find flaky tests (tests that sometimes pass, sometimes fail)
|
|
56
|
+
function findFlakyTests(results) {
|
|
57
|
+
const testResults = {};
|
|
58
|
+
|
|
59
|
+
results.forEach((result) => {
|
|
60
|
+
if (!testResults[result.test]) {
|
|
61
|
+
testResults[result.test] = { passed: 0, failed: 0, total: 0 };
|
|
62
|
+
}
|
|
63
|
+
testResults[result.test].total++;
|
|
64
|
+
if (result.status === "passed") testResults[result.test].passed++;
|
|
65
|
+
if (result.status === "failed") testResults[result.test].failed++;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return Object.entries(testResults)
|
|
69
|
+
.filter(([test, stats]) => stats.passed > 0 && stats.failed > 0)
|
|
70
|
+
.map(([test, stats]) => ({
|
|
71
|
+
test,
|
|
72
|
+
passRate: ((stats.passed / stats.total) * 100).toFixed(1),
|
|
73
|
+
runs: stats.total,
|
|
74
|
+
}))
|
|
75
|
+
.sort((a, b) => parseFloat(a.passRate) - parseFloat(b.passRate));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Performance trends
|
|
79
|
+
function getPerformanceTrends(results, testName) {
|
|
80
|
+
return results
|
|
81
|
+
.filter((r) => r.test === testName)
|
|
82
|
+
.slice(-10) // Last 10 runs
|
|
83
|
+
.map((r) => ({
|
|
84
|
+
timestamp: new Date(r.timestamp).toLocaleString(),
|
|
85
|
+
duration: r.duration,
|
|
86
|
+
status: r.status,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Main
|
|
91
|
+
const command = process.argv[2] || "summary";
|
|
92
|
+
const pillar = process.argv[3] || "synthetic";
|
|
93
|
+
const environment = process.argv[4] || "local";
|
|
94
|
+
|
|
95
|
+
console.log(`\n📊 Test Results - ${pillar}/${environment}\n`);
|
|
96
|
+
|
|
97
|
+
const results = readResults(pillar, environment);
|
|
98
|
+
|
|
99
|
+
if (results.length === 0) {
|
|
100
|
+
console.log("No results found. Run some tests first!\n");
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (command === "summary") {
|
|
105
|
+
const stats = calculateStats(results);
|
|
106
|
+
console.log(`Total Tests: ${stats.total}`);
|
|
107
|
+
console.log(`✅ Passed: ${stats.passed} (${stats.passRate}%)`);
|
|
108
|
+
console.log(`❌ Failed: ${stats.failed}`);
|
|
109
|
+
console.log(`⏭️ Skipped: ${stats.skipped}`);
|
|
110
|
+
console.log(`⏱️ Avg Duration: ${stats.avgDuration.toFixed(0)}ms\n`);
|
|
111
|
+
|
|
112
|
+
// Recent failures
|
|
113
|
+
const recentFailures = results.filter((r) => r.status === "failed").slice(-5);
|
|
114
|
+
if (recentFailures.length > 0) {
|
|
115
|
+
console.log("Recent Failures:");
|
|
116
|
+
recentFailures.forEach((r) => {
|
|
117
|
+
console.log(` • ${r.test}`);
|
|
118
|
+
if (r.error) console.log(` ${r.error.substring(0, 80)}...`);
|
|
119
|
+
});
|
|
120
|
+
console.log();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (command === "flaky") {
|
|
125
|
+
const flaky = findFlakyTests(results);
|
|
126
|
+
if (flaky.length === 0) {
|
|
127
|
+
console.log("✅ No flaky tests detected!\n");
|
|
128
|
+
} else {
|
|
129
|
+
console.log(`⚠️ Found ${flaky.length} flaky tests:\n`);
|
|
130
|
+
flaky.forEach(({ test, passRate, runs }) => {
|
|
131
|
+
console.log(` ${passRate}% pass rate (${runs} runs)`);
|
|
132
|
+
console.log(` ${test}\n`);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (command === "trends") {
|
|
138
|
+
const testName = process.argv[5];
|
|
139
|
+
if (!testName) {
|
|
140
|
+
console.log(
|
|
141
|
+
"Usage: node view-results.js trends <pillar> <environment> <testName>\n",
|
|
142
|
+
);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const trends = getPerformanceTrends(results, testName);
|
|
147
|
+
console.log(`Performance trend for: ${testName}\n`);
|
|
148
|
+
trends.forEach(({ timestamp, duration, status }) => {
|
|
149
|
+
const statusIcon = status === "passed" ? "✅" : "❌";
|
|
150
|
+
console.log(`${statusIcon} ${timestamp}: ${duration}ms`);
|
|
151
|
+
});
|
|
152
|
+
console.log();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`📁 Results file: results/${pillar}-${environment}.jsonl`);
|
|
156
|
+
console.log(`📅 Showing last 7 days\n`);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run the web UI as a long-lived macOS service (a launchd LaunchAgent):
|
|
3
|
+
# started at login, restarted if it exits, not tied to any terminal.
|
|
4
|
+
#
|
|
5
|
+
# service/install.sh install (or refresh) and start it
|
|
6
|
+
# service/install.sh uninstall stop it and remove the LaunchAgent
|
|
7
|
+
# service/install.sh status launchd's view of it
|
|
8
|
+
# service/install.sh print write the LaunchAgent plist to stdout only
|
|
9
|
+
#
|
|
10
|
+
# Works from Den's checkout and from a distribution, where it lives in
|
|
11
|
+
# node_modules/@monster-orange/den/service/. The distribution root is
|
|
12
|
+
# DEN_ROOT, else this checkout when it holds den.yml. The agent runs the
|
|
13
|
+
# engine's server with the node found on PATH now, from the distribution
|
|
14
|
+
# root; re-run install after changing node versions.
|
|
15
|
+
#
|
|
16
|
+
# DEN_SERVICE_LABEL launchd label (default local.<dist dir name>.web)
|
|
17
|
+
# DEN_SERVICE_LOGS log directory (default ~/Library/Logs/<dist dir name>)
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
21
|
+
ENGINE="$(cd "$HERE/.." && pwd)"
|
|
22
|
+
if [ -n "${DEN_ROOT:-}" ]; then
|
|
23
|
+
DIST="$(cd "$DEN_ROOT" && pwd)"
|
|
24
|
+
elif [ -f "$ENGINE/den.yml" ]; then
|
|
25
|
+
DIST="$ENGINE"
|
|
26
|
+
else
|
|
27
|
+
echo "✗ set DEN_ROOT to your distribution's root (the directory with den.yml)" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
[ -f "$DIST/den.yml" ] || { echo "✗ $DIST has no den.yml" >&2; exit 1; }
|
|
31
|
+
NAME="$(basename "$DIST")"
|
|
32
|
+
LABEL="${DEN_SERVICE_LABEL:-local.$NAME.web}"
|
|
33
|
+
LOGS="${DEN_SERVICE_LOGS:-$HOME/Library/Logs/$NAME}"
|
|
34
|
+
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
|
35
|
+
DOMAIN="gui/$(id -u)"
|
|
36
|
+
|
|
37
|
+
# tsx: the nearest node_modules/tsx at or above the engine (hoisted installs).
|
|
38
|
+
find_tsx() {
|
|
39
|
+
local d="$ENGINE"
|
|
40
|
+
while :; do
|
|
41
|
+
if [ -f "$d/node_modules/tsx/package.json" ]; then echo "$d/node_modules/tsx"; return 0; fi
|
|
42
|
+
[ "$d" = "/" ] && return 1
|
|
43
|
+
d="$(dirname "$d")"
|
|
44
|
+
done
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
write_plist() { # $1 = output file
|
|
48
|
+
NODE="$(command -v node)" || { echo "✗ node is not on PATH" >&2; exit 1; }
|
|
49
|
+
TSX="$(find_tsx)" || { echo "✗ tsx not installed: run npm ci in $DIST" >&2; exit 1; }
|
|
50
|
+
DOCKER_DIR="$(dirname "$(command -v docker || echo /usr/local/bin/docker)")"
|
|
51
|
+
SVC_PATH="$(dirname "$NODE"):$DOCKER_DIR:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
52
|
+
python3 -I - "$1" "$LABEL" "$NODE" "$TSX" "$ENGINE" "$DIST" "$SVC_PATH" "$HOME" "$LOGS" <<'PY'
|
|
53
|
+
import plistlib, sys
|
|
54
|
+
out, label, node, tsx, engine, dist, path, home, logs = sys.argv[1:]
|
|
55
|
+
data = {
|
|
56
|
+
"Label": label,
|
|
57
|
+
"ProgramArguments": [node, "--import", f"{tsx}/dist/loader.mjs",
|
|
58
|
+
f"{engine}/src/ui/server/index.ts", "--silent"],
|
|
59
|
+
"WorkingDirectory": dist,
|
|
60
|
+
"EnvironmentVariables": {"PATH": path, "HOME": home,
|
|
61
|
+
"DEN_ROOT": dist, "DEN_ENGINE_ROOT": engine},
|
|
62
|
+
"RunAtLoad": True,
|
|
63
|
+
"KeepAlive": True,
|
|
64
|
+
"ThrottleInterval": 10,
|
|
65
|
+
"ProcessType": "Background",
|
|
66
|
+
"StandardOutPath": f"{logs}/web.log",
|
|
67
|
+
"StandardErrorPath": f"{logs}/web.log",
|
|
68
|
+
}
|
|
69
|
+
if out == "-":
|
|
70
|
+
sys.stdout.buffer.write(plistlib.dumps(data))
|
|
71
|
+
else:
|
|
72
|
+
with open(out, "wb") as f:
|
|
73
|
+
plistlib.dump(data, f)
|
|
74
|
+
PY
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "${1:-install}" in
|
|
78
|
+
install)
|
|
79
|
+
mkdir -p "$LOGS" "$(dirname "$PLIST")"
|
|
80
|
+
write_plist "$PLIST.new"
|
|
81
|
+
plutil -lint "$PLIST.new" >/dev/null
|
|
82
|
+
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
|
|
83
|
+
# bootout returns before launchd has let go of the label; a bootstrap
|
|
84
|
+
# straight after fails ("Bootstrap failed: 5") and leaves NO service.
|
|
85
|
+
# Wait for it to go, then retry bootstrap a few times.
|
|
86
|
+
for _ in $(seq 1 50); do
|
|
87
|
+
launchctl print "$DOMAIN/$LABEL" >/dev/null 2>&1 || break
|
|
88
|
+
sleep 0.2
|
|
89
|
+
done
|
|
90
|
+
mv "$PLIST.new" "$PLIST"
|
|
91
|
+
loaded=0
|
|
92
|
+
for _ in 1 2 3 4 5; do
|
|
93
|
+
if launchctl bootstrap "$DOMAIN" "$PLIST" 2>/dev/null; then loaded=1; break; fi
|
|
94
|
+
sleep 1
|
|
95
|
+
done
|
|
96
|
+
[ "$loaded" = 1 ] || { echo "✗ launchctl bootstrap failed for $LABEL (plist: $PLIST)" >&2; exit 1; }
|
|
97
|
+
launchctl kickstart -k "$DOMAIN/$LABEL"
|
|
98
|
+
echo "✓ $LABEL running (logs: $LOGS/web.log)"
|
|
99
|
+
;;
|
|
100
|
+
uninstall)
|
|
101
|
+
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
|
|
102
|
+
rm -f "$PLIST"
|
|
103
|
+
echo "✓ $LABEL removed"
|
|
104
|
+
;;
|
|
105
|
+
print)
|
|
106
|
+
write_plist -
|
|
107
|
+
;;
|
|
108
|
+
status)
|
|
109
|
+
launchctl print "$DOMAIN/$LABEL" | grep -E '^\s*(state|pid|last exit code|runs)' || echo "$LABEL is not loaded"
|
|
110
|
+
;;
|
|
111
|
+
*) echo "usage: $0 [install|uninstall|status|print]" >&2; exit 2 ;;
|
|
112
|
+
esac
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Playwright Fixtures for Integration (API) Tests
|
|
3
|
+
*
|
|
4
|
+
* Auth model: Authorization: Bearer <jwt>
|
|
5
|
+
* Tokens are minted by the environment's token provider (auth0, cognito,
|
|
6
|
+
* local-jwt) and cached to disk. See shared/auth/api-token.ts for details.
|
|
7
|
+
*
|
|
8
|
+
* Prerequisites:
|
|
9
|
+
* - Credentials available: the secrets backend (test_accounts in the org
|
|
10
|
+
* config) or CI ENV vars
|
|
11
|
+
* - For auth0: the "Password" grant type enabled for the app
|
|
12
|
+
* - Social-login accounts cannot be used for API token tests
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { test as base, request } from "@playwright/test";
|
|
16
|
+
import { envSelector } from "../../../shared/environment/env-selector.js";
|
|
17
|
+
import { getApiToken } from "../../../shared/auth/api-token.js";
|
|
18
|
+
import { openApiValidator } from "./openapi-validator.js";
|
|
19
|
+
import type {
|
|
20
|
+
Environment,
|
|
21
|
+
EnvironmentConfig,
|
|
22
|
+
} from "../../../shared/types/index.js";
|
|
23
|
+
import type { APIRequestContext } from "@playwright/test";
|
|
24
|
+
|
|
25
|
+
type CustomFixtures = {
|
|
26
|
+
environment: Environment;
|
|
27
|
+
envConfig: EnvironmentConfig;
|
|
28
|
+
/** Unauthenticated API context — for testing public endpoints or 401 behavior */
|
|
29
|
+
apiContext: APIRequestContext;
|
|
30
|
+
/** Admin JWT bearer token */
|
|
31
|
+
adminToken: string;
|
|
32
|
+
/** User JWT bearer token */
|
|
33
|
+
userToken: string;
|
|
34
|
+
/** Authenticated API context using admin token (Bearer) */
|
|
35
|
+
adminContext: APIRequestContext;
|
|
36
|
+
/** Authenticated API context using user token (Bearer) */
|
|
37
|
+
userContext: APIRequestContext;
|
|
38
|
+
/** Alias for adminContext — backward compatibility */
|
|
39
|
+
authenticatedContext: APIRequestContext;
|
|
40
|
+
/** Alias for adminToken — backward compatibility */
|
|
41
|
+
accessToken: string;
|
|
42
|
+
validator: typeof openApiValidator;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const test = base.extend<CustomFixtures>({
|
|
46
|
+
// eslint-disable-next-line no-empty-pattern
|
|
47
|
+
environment: async ({}, use) => {
|
|
48
|
+
const env = (process.env.E2E_ENVIRONMENT as Environment) || "local";
|
|
49
|
+
await use(env);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
envConfig: async ({ environment }, use) => {
|
|
53
|
+
const config = await envSelector.loadEnvironment(environment);
|
|
54
|
+
await use(config);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// ── JWT token fixtures ────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
adminToken: async ({ environment }, use) => {
|
|
60
|
+
const token = await getApiToken("admin", environment);
|
|
61
|
+
await use(token);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
userToken: async ({ environment }, use) => {
|
|
65
|
+
const token = await getApiToken("user", environment);
|
|
66
|
+
await use(token);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// Backward-compat alias
|
|
70
|
+
accessToken: async ({ adminToken }, use) => {
|
|
71
|
+
await use(adminToken);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// ── Request context fixtures ──────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/** Unauthenticated — use for testing 401 responses or public endpoints */
|
|
77
|
+
apiContext: async ({ envConfig }, use) => {
|
|
78
|
+
const context = await request.newContext({
|
|
79
|
+
baseURL: envConfig.baseUrls.api,
|
|
80
|
+
extraHTTPHeaders: {
|
|
81
|
+
Accept: "application/json",
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
await use(context);
|
|
86
|
+
await context.dispose();
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/** Admin-authenticated API context — use for admin-only endpoints */
|
|
90
|
+
adminContext: async ({ envConfig, adminToken }, use) => {
|
|
91
|
+
const context = await request.newContext({
|
|
92
|
+
baseURL: envConfig.baseUrls.api,
|
|
93
|
+
extraHTTPHeaders: {
|
|
94
|
+
Accept: "application/json",
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
Authorization: `Bearer ${adminToken}`,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
await use(context);
|
|
100
|
+
await context.dispose();
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/** User-authenticated API context — use for user-scoped endpoints */
|
|
104
|
+
userContext: async ({ envConfig, userToken }, use) => {
|
|
105
|
+
const context = await request.newContext({
|
|
106
|
+
baseURL: envConfig.baseUrls.api,
|
|
107
|
+
extraHTTPHeaders: {
|
|
108
|
+
Accept: "application/json",
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
Authorization: `Bearer ${userToken}`,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
await use(context);
|
|
114
|
+
await context.dispose();
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// Backward-compat alias
|
|
118
|
+
authenticatedContext: async ({ adminContext }, use) => {
|
|
119
|
+
await use(adminContext);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// eslint-disable-next-line no-empty-pattern
|
|
123
|
+
validator: async ({}, use) => {
|
|
124
|
+
await use(openApiValidator);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export { expect } from "@playwright/test";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI Schema Validator
|
|
3
|
+
* Validates API responses against OpenAPI schemas
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Ajv from "ajv";
|
|
7
|
+
import addFormats from "ajv-formats";
|
|
8
|
+
import { readFile } from "fs/promises";
|
|
9
|
+
import { resolve } from "path";
|
|
10
|
+
import { enginePath } from "../../../shared/engine.js";
|
|
11
|
+
|
|
12
|
+
export class OpenAPIValidator {
|
|
13
|
+
private ajv: Ajv;
|
|
14
|
+
private schemas: Map<string, unknown> = new Map();
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
this.ajv = new Ajv({
|
|
18
|
+
allErrors: true,
|
|
19
|
+
verbose: true,
|
|
20
|
+
strict: false,
|
|
21
|
+
});
|
|
22
|
+
addFormats(this.ajv);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load OpenAPI spec from file
|
|
27
|
+
*/
|
|
28
|
+
async loadSpec(specPath: string): Promise<void> {
|
|
29
|
+
// read-only schema load from Den's own files, so worker cwd is irrelevant
|
|
30
|
+
const fullPath = resolve(enginePath("src/pillars/api/schemas"), specPath);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const content = await readFile(fullPath, "utf-8");
|
|
34
|
+
const spec = JSON.parse(content);
|
|
35
|
+
|
|
36
|
+
// Extract schemas from OpenAPI spec
|
|
37
|
+
if (spec.components && spec.components.schemas) {
|
|
38
|
+
for (const [name, schema] of Object.entries(spec.components.schemas)) {
|
|
39
|
+
this.schemas.set(name, schema as object);
|
|
40
|
+
this.ajv.addSchema(schema as object, name);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Failed to load OpenAPI spec from ${specPath}: ${(error as Error).message}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate response data against a schema
|
|
52
|
+
*/
|
|
53
|
+
validate(
|
|
54
|
+
schemaName: string,
|
|
55
|
+
data: unknown,
|
|
56
|
+
): { valid: boolean; errors: string[] } {
|
|
57
|
+
const schema = this.schemas.get(schemaName);
|
|
58
|
+
|
|
59
|
+
if (!schema) {
|
|
60
|
+
return {
|
|
61
|
+
valid: false,
|
|
62
|
+
errors: [
|
|
63
|
+
`Schema "${schemaName}" not found. Available schemas: ${Array.from(this.schemas.keys()).join(", ")}`,
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const valid = this.ajv.validate(schema, data);
|
|
69
|
+
|
|
70
|
+
if (!valid && this.ajv.errors) {
|
|
71
|
+
const errors = this.ajv.errors.map((err) => {
|
|
72
|
+
const path = err.instancePath || "root";
|
|
73
|
+
return `${path}: ${err.message}`;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return { valid: false, errors };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { valid: true, errors: [] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate and throw if invalid
|
|
84
|
+
*/
|
|
85
|
+
validateOrThrow(schemaName: string, data: unknown): void {
|
|
86
|
+
const result = this.validate(schemaName, data);
|
|
87
|
+
|
|
88
|
+
if (!result.valid) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Schema validation failed for "${schemaName}":\n${result.errors.join("\n")}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get list of available schemas
|
|
97
|
+
*/
|
|
98
|
+
getAvailableSchemas(): string[] {
|
|
99
|
+
return Array.from(this.schemas.keys());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if schema exists
|
|
104
|
+
*/
|
|
105
|
+
hasSchema(schemaName: string): boolean {
|
|
106
|
+
return this.schemas.has(schemaName);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Export singleton instance
|
|
111
|
+
export const openApiValidator = new OpenAPIValidator();
|