@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,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL Reporter for Playwright
|
|
3
|
+
* Writes test results to JSONL format for historical tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
Reporter,
|
|
8
|
+
TestCase,
|
|
9
|
+
TestResult as PlaywrightTestResult,
|
|
10
|
+
FullConfig,
|
|
11
|
+
Suite,
|
|
12
|
+
} from "@playwright/test/reporter";
|
|
13
|
+
import { resultWriter } from "../results/result-writer.js";
|
|
14
|
+
import type { Pillar, Environment } from "../types/index.js";
|
|
15
|
+
import * as os from "os";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
|
|
18
|
+
export default class JSONLReporter implements Reporter {
|
|
19
|
+
onBegin(_config: FullConfig, _suite: Suite) {
|
|
20
|
+
console.log("[JSONL Reporter] Initialized");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async onTestEnd(test: TestCase, result: PlaywrightTestResult) {
|
|
24
|
+
console.log("[JSONL Reporter] onTestEnd called");
|
|
25
|
+
try {
|
|
26
|
+
// Detect pillar from test file path
|
|
27
|
+
const pillar = this.detectPillar(test.location.file);
|
|
28
|
+
console.log(`[JSONL Reporter] Pillar detected: ${pillar}`);
|
|
29
|
+
|
|
30
|
+
// Get environment from env var or default to local
|
|
31
|
+
const environment = (process.env.TEST_ENV || "local") as Environment;
|
|
32
|
+
|
|
33
|
+
// Get test title path (e.g., "Smoke Tests - App Health › web app is accessible")
|
|
34
|
+
const testTitle = test.titlePath().slice(1).join(" › ");
|
|
35
|
+
|
|
36
|
+
// Get relative test file path for better readability
|
|
37
|
+
const testFile = this.getRelativeTestPath(test.location.file, pillar);
|
|
38
|
+
|
|
39
|
+
// Determine status
|
|
40
|
+
let status: "passed" | "failed" | "skipped";
|
|
41
|
+
if (result.status === "passed") {
|
|
42
|
+
status = "passed";
|
|
43
|
+
} else if (result.status === "skipped") {
|
|
44
|
+
status = "skipped";
|
|
45
|
+
} else {
|
|
46
|
+
status = "failed";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get screenshot and trace paths if they exist
|
|
50
|
+
const screenshot = result.attachments.find(
|
|
51
|
+
(a) => a.name === "screenshot" || a.contentType?.startsWith("image/"),
|
|
52
|
+
)?.path;
|
|
53
|
+
|
|
54
|
+
const trace = result.attachments.find(
|
|
55
|
+
(a) => a.name === "trace" || a.contentType?.includes("zip"),
|
|
56
|
+
)?.path;
|
|
57
|
+
|
|
58
|
+
// Write result to JSONL
|
|
59
|
+
await resultWriter.writeResult({
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
|
+
pillar,
|
|
62
|
+
environment,
|
|
63
|
+
test: `${testFile} › ${testTitle}`,
|
|
64
|
+
status,
|
|
65
|
+
duration: result.duration,
|
|
66
|
+
user: process.env.USER || os.userInfo().username || "unknown",
|
|
67
|
+
error: result.error?.message,
|
|
68
|
+
screenshot,
|
|
69
|
+
trace,
|
|
70
|
+
metadata: {
|
|
71
|
+
browser: test.parent.project()?.name,
|
|
72
|
+
retries: result.retry,
|
|
73
|
+
workerIndex: result.workerIndex,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// Don't let reporter errors break tests
|
|
78
|
+
console.warn(
|
|
79
|
+
`[JSONL Reporter] Failed to write result: ${(error as Error).message}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Detect pillar from test file path
|
|
86
|
+
*/
|
|
87
|
+
private detectPillar(filePath: string): Pillar {
|
|
88
|
+
if (
|
|
89
|
+
filePath.includes("/.synth-run/") ||
|
|
90
|
+
filePath.includes("/pillars/browser/")
|
|
91
|
+
) {
|
|
92
|
+
return "synthetic";
|
|
93
|
+
} else if (
|
|
94
|
+
filePath.includes("/pillars/api/") ||
|
|
95
|
+
filePath.includes("/integration/")
|
|
96
|
+
) {
|
|
97
|
+
return "integration";
|
|
98
|
+
} else if (
|
|
99
|
+
filePath.includes("/pillars/perf/") ||
|
|
100
|
+
filePath.includes("/performance/")
|
|
101
|
+
) {
|
|
102
|
+
return "performance";
|
|
103
|
+
} else if (
|
|
104
|
+
filePath.includes("/pillars/smoke/") ||
|
|
105
|
+
filePath.includes("/.smoke-run/")
|
|
106
|
+
) {
|
|
107
|
+
return "smoke";
|
|
108
|
+
} else if (filePath.includes("/pillars/journeys/")) {
|
|
109
|
+
// Journeys aren't Playwright-run (they have their own JSONL writer in
|
|
110
|
+
// src/pillars/journeys/report/); branch kept for union completeness.
|
|
111
|
+
return "journeys";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Default to synthetic
|
|
115
|
+
return "synthetic";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get relative test path for display
|
|
120
|
+
*/
|
|
121
|
+
private getRelativeTestPath(filePath: string, pillar: Pillar): string {
|
|
122
|
+
// Extract path relative to pillar directory
|
|
123
|
+
const parts = filePath.split(`/${pillar}/tests/`);
|
|
124
|
+
if (parts.length > 1) {
|
|
125
|
+
return parts[1].replace(/\.spec\.ts$/, "");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Fallback to just filename
|
|
129
|
+
return path.basename(filePath, ".spec.ts");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Reporter
|
|
3
|
+
* Sends test results to Slack via webhook
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readBrand } from "../brand.js";
|
|
7
|
+
import { IncomingWebhook, IncomingWebhookSendArguments } from "@slack/webhook";
|
|
8
|
+
import { readFile } from "fs/promises";
|
|
9
|
+
import { resolve } from "path";
|
|
10
|
+
import type { TestSummary, Pillar } from "../types/index.js";
|
|
11
|
+
|
|
12
|
+
export interface SlackConfig {
|
|
13
|
+
webhookUrl: string;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
notifyOn: {
|
|
16
|
+
testComplete: boolean;
|
|
17
|
+
testFailure: boolean;
|
|
18
|
+
thresholdExceeded: boolean;
|
|
19
|
+
};
|
|
20
|
+
thresholds: {
|
|
21
|
+
failurePercentage: number;
|
|
22
|
+
};
|
|
23
|
+
channel: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SlackReporter {
|
|
27
|
+
private webhook: IncomingWebhook | null = null;
|
|
28
|
+
private config: SlackConfig | null = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load Slack configuration
|
|
32
|
+
*/
|
|
33
|
+
async loadConfig(): Promise<void> {
|
|
34
|
+
// cwd-derived path is READ-ONLY: optional notify config lookup; nothing is
|
|
35
|
+
// ever written here (and the unit test pins cwd via jest).
|
|
36
|
+
const configPath = resolve(process.cwd(), "config/slack.json");
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const content = await readFile(configPath, "utf-8");
|
|
40
|
+
this.config = JSON.parse(content);
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
this.config?.enabled &&
|
|
44
|
+
this.config.webhookUrl !== "REPLACE_WITH_SLACK_WEBHOOK_URL"
|
|
45
|
+
) {
|
|
46
|
+
this.webhook = new IncomingWebhook(this.config.webhookUrl);
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn("Failed to load Slack config:", (error as Error).message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if Slack reporting is enabled and configured
|
|
55
|
+
*/
|
|
56
|
+
isEnabled(): boolean {
|
|
57
|
+
return this.webhook !== null && this.config !== null && this.config.enabled;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Send test summary to Slack
|
|
62
|
+
*/
|
|
63
|
+
async sendTestSummary(
|
|
64
|
+
pillar: Pillar,
|
|
65
|
+
environment: string,
|
|
66
|
+
summary: TestSummary,
|
|
67
|
+
reportUrl?: string,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
if (!this.isEnabled() || !this.config) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const failurePercentage =
|
|
74
|
+
summary.total > 0 ? (summary.failed / summary.total) * 100 : 0;
|
|
75
|
+
const shouldNotify = this.shouldNotify(summary, failurePercentage);
|
|
76
|
+
|
|
77
|
+
if (!shouldNotify) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const message = this.formatTestSummary(
|
|
82
|
+
pillar,
|
|
83
|
+
environment,
|
|
84
|
+
summary,
|
|
85
|
+
reportUrl,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await this.webhook!.send(message);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(
|
|
92
|
+
"Failed to send Slack notification:",
|
|
93
|
+
(error as Error).message,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Send test failure notification
|
|
100
|
+
*/
|
|
101
|
+
async sendTestFailure(
|
|
102
|
+
pillar: Pillar,
|
|
103
|
+
environment: string,
|
|
104
|
+
testName: string,
|
|
105
|
+
errorMessage: string,
|
|
106
|
+
reportUrl?: string,
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
if (!this.isEnabled() || !this.config?.notifyOn.testFailure) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const message = this.formatTestFailure(
|
|
113
|
+
pillar,
|
|
114
|
+
environment,
|
|
115
|
+
testName,
|
|
116
|
+
errorMessage,
|
|
117
|
+
reportUrl,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await this.webhook!.send(message);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(
|
|
124
|
+
"Failed to send Slack notification:",
|
|
125
|
+
(error as Error).message,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Determine if we should send notification based on config
|
|
132
|
+
*/
|
|
133
|
+
private shouldNotify(
|
|
134
|
+
summary: TestSummary,
|
|
135
|
+
failurePercentage: number,
|
|
136
|
+
): boolean {
|
|
137
|
+
if (!this.config) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Always notify on threshold exceeded
|
|
142
|
+
if (
|
|
143
|
+
this.config.notifyOn.thresholdExceeded &&
|
|
144
|
+
failurePercentage >= this.config.thresholds.failurePercentage
|
|
145
|
+
) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Notify on test complete if enabled
|
|
150
|
+
if (this.config.notifyOn.testComplete) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Notify on any failure if enabled
|
|
155
|
+
if (this.config.notifyOn.testFailure && summary.failed > 0) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Format test summary as Slack message
|
|
164
|
+
*/
|
|
165
|
+
private formatTestSummary(
|
|
166
|
+
pillar: Pillar,
|
|
167
|
+
environment: string,
|
|
168
|
+
summary: TestSummary,
|
|
169
|
+
reportUrl?: string,
|
|
170
|
+
): IncomingWebhookSendArguments {
|
|
171
|
+
const passPercentage = (
|
|
172
|
+
summary.total > 0 ? (summary.passed / summary.total) * 100 : 0
|
|
173
|
+
).toFixed(1);
|
|
174
|
+
const emoji = summary.failed === 0 ? ":white_check_mark:" : ":warning:";
|
|
175
|
+
const color = summary.failed === 0 ? "#36a64f" : "#ff9800";
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
username: `${readBrand().name} E2E`,
|
|
179
|
+
icon_emoji: ":robot_face:",
|
|
180
|
+
channel: this.config?.channel,
|
|
181
|
+
attachments: [
|
|
182
|
+
{
|
|
183
|
+
color,
|
|
184
|
+
title: `${emoji} ${this.formatPillar(pillar)} Tests - ${environment}`,
|
|
185
|
+
fields: [
|
|
186
|
+
{
|
|
187
|
+
title: "Status",
|
|
188
|
+
value:
|
|
189
|
+
summary.failed === 0
|
|
190
|
+
? "All Passed"
|
|
191
|
+
: `${summary.failed} Failed`,
|
|
192
|
+
short: true,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
title: "Pass Rate",
|
|
196
|
+
value: `${passPercentage}%`,
|
|
197
|
+
short: true,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
title: "Total Tests",
|
|
201
|
+
value: summary.total.toString(),
|
|
202
|
+
short: true,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
title: "Duration",
|
|
206
|
+
value: this.formatDuration(summary.duration),
|
|
207
|
+
short: true,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
title: "Passed",
|
|
211
|
+
value: summary.passed.toString(),
|
|
212
|
+
short: true,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
title: "Failed",
|
|
216
|
+
value: summary.failed.toString(),
|
|
217
|
+
short: true,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
title: "Skipped",
|
|
221
|
+
value: summary.skipped.toString(),
|
|
222
|
+
short: true,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
title: "Environment",
|
|
226
|
+
value: environment,
|
|
227
|
+
short: true,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
footer: `${readBrand().name} E2E`,
|
|
231
|
+
ts: Math.floor(Date.now() / 1000).toString(),
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
...(reportUrl && {
|
|
235
|
+
text: `<${reportUrl}|View Full Report>`,
|
|
236
|
+
}),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Format test failure as Slack message
|
|
242
|
+
*/
|
|
243
|
+
private formatTestFailure(
|
|
244
|
+
pillar: Pillar,
|
|
245
|
+
environment: string,
|
|
246
|
+
testName: string,
|
|
247
|
+
errorMessage: string,
|
|
248
|
+
reportUrl?: string,
|
|
249
|
+
): IncomingWebhookSendArguments {
|
|
250
|
+
return {
|
|
251
|
+
username: `${readBrand().name} E2E`,
|
|
252
|
+
icon_emoji: ":robot_face:",
|
|
253
|
+
channel: this.config?.channel,
|
|
254
|
+
attachments: [
|
|
255
|
+
{
|
|
256
|
+
color: "#d32f2f",
|
|
257
|
+
title: `:x: Test Failure - ${this.formatPillar(pillar)}`,
|
|
258
|
+
fields: [
|
|
259
|
+
{
|
|
260
|
+
title: "Environment",
|
|
261
|
+
value: environment,
|
|
262
|
+
short: true,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
title: "Test",
|
|
266
|
+
value: testName,
|
|
267
|
+
short: true,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
title: "Error",
|
|
271
|
+
value: this.truncateError(errorMessage),
|
|
272
|
+
short: false,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
footer: `${readBrand().name} E2E`,
|
|
276
|
+
ts: Math.floor(Date.now() / 1000).toString(),
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
...(reportUrl && {
|
|
280
|
+
text: `<${reportUrl}|View Full Report>`,
|
|
281
|
+
}),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Format pillar name for display
|
|
287
|
+
*/
|
|
288
|
+
private formatPillar(pillar: Pillar): string {
|
|
289
|
+
const pillarNames: Record<Pillar, string> = {
|
|
290
|
+
synthetic: "Synthetic (Browser)",
|
|
291
|
+
integration: "Integration (API)",
|
|
292
|
+
performance: "Performance (Load)",
|
|
293
|
+
smoke: "Smoke",
|
|
294
|
+
journeys: "Journeys (Agent E2E)",
|
|
295
|
+
security: "Security (OWASP ZAP)",
|
|
296
|
+
suites: "Repo suites",
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
return pillarNames[pillar] || pillar;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Format duration in human-readable format
|
|
304
|
+
*/
|
|
305
|
+
private formatDuration(ms: number): string {
|
|
306
|
+
if (ms < 1000) {
|
|
307
|
+
return `${ms}ms`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const seconds = Math.floor(ms / 1000);
|
|
311
|
+
const minutes = Math.floor(seconds / 60);
|
|
312
|
+
const hours = Math.floor(minutes / 60);
|
|
313
|
+
|
|
314
|
+
if (hours > 0) {
|
|
315
|
+
const remainingMinutes = minutes % 60;
|
|
316
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (minutes > 0) {
|
|
320
|
+
const remainingSeconds = seconds % 60;
|
|
321
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return `${seconds}s`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Truncate error message if too long
|
|
329
|
+
*/
|
|
330
|
+
private truncateError(error: string, maxLength: number = 500): string {
|
|
331
|
+
if (error.length <= maxLength) {
|
|
332
|
+
return error;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return error.substring(0, maxLength) + "...";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Export singleton instance
|
|
340
|
+
export const slackReporter = new SlackReporter();
|