@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.
Files changed (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +61 -0
  5. package/docs/distributions.md +256 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +99 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +406 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +108 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +729 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/applications.ts +138 -0
  91. package/src/shared/auth/api-token.ts +416 -0
  92. package/src/shared/auth/auth0-helper.ts +284 -0
  93. package/src/shared/auth/headless-login.ts +265 -0
  94. package/src/shared/auth/jwt-helper.ts +186 -0
  95. package/src/shared/auth/sudo-vault.ts +69 -0
  96. package/src/shared/auth/test-accounts.ts +98 -0
  97. package/src/shared/auth/token-injection.ts +184 -0
  98. package/src/shared/auth/token-providers/auth0.ts +80 -0
  99. package/src/shared/auth/token-providers/index.ts +182 -0
  100. package/src/shared/auth/token-refresh.ts +273 -0
  101. package/src/shared/banner.ts +15 -0
  102. package/src/shared/brand.ts +79 -0
  103. package/src/shared/cli-tabs.ts +131 -0
  104. package/src/shared/constants.ts +128 -0
  105. package/src/shared/den-config.ts +566 -0
  106. package/src/shared/dev-metrics/emit.ts +354 -0
  107. package/src/shared/edge/config.ts +229 -0
  108. package/src/shared/edge/render.ts +221 -0
  109. package/src/shared/edge/runtime.ts +268 -0
  110. package/src/shared/edge/types.ts +62 -0
  111. package/src/shared/engine.ts +67 -0
  112. package/src/shared/environment/aws-profiles.ts +49 -0
  113. package/src/shared/environment/env-selector.ts +161 -0
  114. package/src/shared/environment/playwright-env.ts +82 -0
  115. package/src/shared/environment-manager/catalog.ts +77 -0
  116. package/src/shared/environment-manager/clone-guard.ts +163 -0
  117. package/src/shared/environment-manager/dag.ts +286 -0
  118. package/src/shared/environment-manager/default-workspace.ts +182 -0
  119. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  120. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  121. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  122. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  123. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  124. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  125. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  126. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  127. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  128. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  129. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  130. package/src/shared/environment-manager/derivation/text.ts +40 -0
  131. package/src/shared/environment-manager/derivation/types.ts +69 -0
  132. package/src/shared/environment-manager/health.ts +589 -0
  133. package/src/shared/environment-manager/index.ts +6 -0
  134. package/src/shared/environment-manager/load-applications.ts +78 -0
  135. package/src/shared/environment-manager/log-tail.ts +212 -0
  136. package/src/shared/environment-manager/make-targets.ts +78 -0
  137. package/src/shared/environment-manager/opctl.ts +334 -0
  138. package/src/shared/environment-manager/readiness.ts +93 -0
  139. package/src/shared/environment-manager/register-local.ts +72 -0
  140. package/src/shared/environment-manager/secrets-check.ts +260 -0
  141. package/src/shared/environment-manager/service-env.ts +110 -0
  142. package/src/shared/environment-manager/service-runner.ts +1588 -0
  143. package/src/shared/environment-manager/up-plan.ts +302 -0
  144. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  145. package/src/shared/environment-manager/workspaces.ts +860 -0
  146. package/src/shared/manifest/deps.ts +27 -0
  147. package/src/shared/manifest/migrate-manifest.ts +101 -0
  148. package/src/shared/manifest/read-manifest.ts +160 -0
  149. package/src/shared/manifest/schema.ts +299 -0
  150. package/src/shared/manifest/service-links.ts +93 -0
  151. package/src/shared/manifest/validate-manifest.ts +140 -0
  152. package/src/shared/manifest/workspace-manifests.ts +92 -0
  153. package/src/shared/open-url.ts +122 -0
  154. package/src/shared/org.ts +44 -0
  155. package/src/shared/paths.ts +178 -0
  156. package/src/shared/perf/optional-fields.ts +105 -0
  157. package/src/shared/perf/report-normalizer.ts +404 -0
  158. package/src/shared/perf/report-paths.ts +19 -0
  159. package/src/shared/perf/report-store.ts +110 -0
  160. package/src/shared/perf/report-types.ts +70 -0
  161. package/src/shared/perf/test-paths.ts +25 -0
  162. package/src/shared/process/log-rotation.ts +147 -0
  163. package/src/shared/process/spawn-streaming.ts +229 -0
  164. package/src/shared/product.ts +92 -0
  165. package/src/shared/profile.ts +141 -0
  166. package/src/shared/repo-kind.ts +32 -0
  167. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  168. package/src/shared/reporting/slack-reporter.ts +340 -0
  169. package/src/shared/results/result-reader.ts +266 -0
  170. package/src/shared/results/result-writer.ts +168 -0
  171. package/src/shared/secrets/doppler.ts +126 -0
  172. package/src/shared/secrets/env.ts +48 -0
  173. package/src/shared/secrets/index.ts +197 -0
  174. package/src/shared/secrets/keys.ts +33 -0
  175. package/src/shared/secrets/onepassword.ts +75 -0
  176. package/src/shared/secrets/types.ts +52 -0
  177. package/src/shared/service-url.ts +119 -0
  178. package/src/shared/smoke/branch-resolver.ts +141 -0
  179. package/src/shared/smoke/git-remote.ts +70 -0
  180. package/src/shared/smoke/repo-cloner.ts +151 -0
  181. package/src/shared/stacks/config.ts +137 -0
  182. package/src/shared/stacks/manifest-commands.ts +88 -0
  183. package/src/shared/stacks/render.ts +253 -0
  184. package/src/shared/stacks/runtime.ts +334 -0
  185. package/src/shared/stacks/types.ts +53 -0
  186. package/src/shared/terminal/tabs.ts +212 -0
  187. package/src/shared/test-discovery/module-scanner.ts +569 -0
  188. package/src/shared/types/index.ts +678 -0
  189. package/src/shared/ui-marker.ts +19 -0
  190. package/src/ui/cli/applications-command.ts +116 -0
  191. package/src/ui/cli/config-command.ts +68 -0
  192. package/src/ui/cli/create-main.ts +9 -0
  193. package/src/ui/cli/create.ts +360 -0
  194. package/src/ui/cli/edge-command.ts +91 -0
  195. package/src/ui/cli/flag-parser.ts +246 -0
  196. package/src/ui/cli/graph-command.ts +524 -0
  197. package/src/ui/cli/health-display.ts +63 -0
  198. package/src/ui/cli/index.ts +3656 -0
  199. package/src/ui/cli/init.ts +855 -0
  200. package/src/ui/cli/invocation-target.ts +175 -0
  201. package/src/ui/cli/logs-command.ts +104 -0
  202. package/src/ui/cli/module-selection.ts +31 -0
  203. package/src/ui/cli/prompts.ts +767 -0
  204. package/src/ui/cli/results-viewer.ts +436 -0
  205. package/src/ui/cli/secrets-preflight.ts +91 -0
  206. package/src/ui/cli/stack-command.ts +144 -0
  207. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  208. package/src/ui/cli/test-runner.ts +1312 -0
  209. package/src/ui/cli/web-command.ts +1073 -0
  210. package/src/ui/server/changelog-parser.ts +94 -0
  211. package/src/ui/server/daemon-reaper.ts +129 -0
  212. package/src/ui/server/editor-manager.ts +301 -0
  213. package/src/ui/server/github-service.ts +223 -0
  214. package/src/ui/server/http-helpers.ts +98 -0
  215. package/src/ui/server/index.ts +3236 -0
  216. package/src/ui/server/log-history.ts +452 -0
  217. package/src/ui/server/perf-report.ts +42 -0
  218. package/src/ui/server/public/environments.html +494 -0
  219. package/src/ui/server/public/favicon.ico +0 -0
  220. package/src/ui/server/public/header.js +1371 -0
  221. package/src/ui/server/public/index.html +2595 -0
  222. package/src/ui/server/public/local-dev.html +7405 -0
  223. package/src/ui/server/public/logs.html +1489 -0
  224. package/src/ui/server/public/perf-report.js +359 -0
  225. package/src/ui/server/public/release.html +2111 -0
  226. package/src/ui/server/public/services.html +1098 -0
  227. package/src/ui/server/public/workspace-picker.js +69 -0
  228. package/src/ui/server/release-deploy-session.ts +246 -0
  229. package/src/ui/server/release-service.ts +1125 -0
  230. package/src/ui/server/sudo-store.ts +111 -0
  231. 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();