@monster-orange/den 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +68 -0
  5. package/docs/distributions.md +499 -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 +101 -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 +403 -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 +124 -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 +757 -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/agent-actor.ts +77 -0
  91. package/src/shared/applications.ts +138 -0
  92. package/src/shared/auth/api-token.ts +416 -0
  93. package/src/shared/auth/auth0-helper.ts +284 -0
  94. package/src/shared/auth/headless-login.ts +265 -0
  95. package/src/shared/auth/jwt-helper.ts +186 -0
  96. package/src/shared/auth/sudo-vault.ts +69 -0
  97. package/src/shared/auth/test-accounts.ts +98 -0
  98. package/src/shared/auth/token-injection.ts +184 -0
  99. package/src/shared/auth/token-providers/auth0.ts +80 -0
  100. package/src/shared/auth/token-providers/index.ts +182 -0
  101. package/src/shared/auth/token-refresh.ts +273 -0
  102. package/src/shared/banner.ts +15 -0
  103. package/src/shared/brand.ts +79 -0
  104. package/src/shared/cli-tabs.ts +131 -0
  105. package/src/shared/constants.ts +128 -0
  106. package/src/shared/den-config.ts +688 -0
  107. package/src/shared/dev-metrics/emit.ts +354 -0
  108. package/src/shared/edge/config.ts +229 -0
  109. package/src/shared/edge/render.ts +221 -0
  110. package/src/shared/edge/runtime.ts +268 -0
  111. package/src/shared/edge/status.ts +74 -0
  112. package/src/shared/edge/types.ts +62 -0
  113. package/src/shared/engine.ts +67 -0
  114. package/src/shared/environment/aws-profiles.ts +49 -0
  115. package/src/shared/environment/env-selector.ts +161 -0
  116. package/src/shared/environment/playwright-env.ts +82 -0
  117. package/src/shared/environment-manager/catalog.ts +77 -0
  118. package/src/shared/environment-manager/clone-guard.ts +163 -0
  119. package/src/shared/environment-manager/dag.ts +286 -0
  120. package/src/shared/environment-manager/default-workspace.ts +182 -0
  121. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  122. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  123. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  124. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  125. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  126. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  127. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  128. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  129. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  130. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  131. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  132. package/src/shared/environment-manager/derivation/text.ts +40 -0
  133. package/src/shared/environment-manager/derivation/types.ts +69 -0
  134. package/src/shared/environment-manager/health.ts +635 -0
  135. package/src/shared/environment-manager/index.ts +6 -0
  136. package/src/shared/environment-manager/load-applications.ts +108 -0
  137. package/src/shared/environment-manager/log-tail.ts +213 -0
  138. package/src/shared/environment-manager/make-targets.ts +78 -0
  139. package/src/shared/environment-manager/opctl.ts +335 -0
  140. package/src/shared/environment-manager/readiness.ts +93 -0
  141. package/src/shared/environment-manager/register-local.ts +72 -0
  142. package/src/shared/environment-manager/secrets-check.ts +260 -0
  143. package/src/shared/environment-manager/service-container.ts +47 -0
  144. package/src/shared/environment-manager/service-env.ts +110 -0
  145. package/src/shared/environment-manager/service-runner.ts +1603 -0
  146. package/src/shared/environment-manager/sync-targets.ts +66 -0
  147. package/src/shared/environment-manager/up-plan.ts +326 -0
  148. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  149. package/src/shared/environment-manager/workspaces.ts +860 -0
  150. package/src/shared/manifest/deps.ts +27 -0
  151. package/src/shared/manifest/merge-override.ts +75 -0
  152. package/src/shared/manifest/migrate-manifest.ts +101 -0
  153. package/src/shared/manifest/placement.ts +321 -0
  154. package/src/shared/manifest/read-manifest.ts +278 -0
  155. package/src/shared/manifest/schema.ts +313 -0
  156. package/src/shared/manifest/service-links.ts +93 -0
  157. package/src/shared/manifest/validate-layers.ts +229 -0
  158. package/src/shared/manifest/validate-manifest.ts +200 -0
  159. package/src/shared/manifest/workspace-manifests.ts +92 -0
  160. package/src/shared/open-url.ts +122 -0
  161. package/src/shared/org.ts +44 -0
  162. package/src/shared/paths.ts +190 -0
  163. package/src/shared/perf/optional-fields.ts +105 -0
  164. package/src/shared/perf/report-normalizer.ts +404 -0
  165. package/src/shared/perf/report-paths.ts +19 -0
  166. package/src/shared/perf/report-store.ts +110 -0
  167. package/src/shared/perf/report-types.ts +70 -0
  168. package/src/shared/perf/test-paths.ts +25 -0
  169. package/src/shared/process/log-rotation.ts +147 -0
  170. package/src/shared/process/spawn-streaming.ts +229 -0
  171. package/src/shared/product.ts +92 -0
  172. package/src/shared/profile.ts +141 -0
  173. package/src/shared/redact.ts +107 -0
  174. package/src/shared/repo-kind.ts +32 -0
  175. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  176. package/src/shared/reporting/slack-reporter.ts +340 -0
  177. package/src/shared/results/result-reader.ts +266 -0
  178. package/src/shared/results/result-writer.ts +168 -0
  179. package/src/shared/secrets/doppler.ts +126 -0
  180. package/src/shared/secrets/env.ts +48 -0
  181. package/src/shared/secrets/index.ts +197 -0
  182. package/src/shared/secrets/keys.ts +33 -0
  183. package/src/shared/secrets/onepassword.ts +75 -0
  184. package/src/shared/secrets/types.ts +52 -0
  185. package/src/shared/service-url.ts +119 -0
  186. package/src/shared/smoke/branch-resolver.ts +141 -0
  187. package/src/shared/smoke/git-remote.ts +70 -0
  188. package/src/shared/smoke/repo-cloner.ts +151 -0
  189. package/src/shared/stacks/config.ts +137 -0
  190. package/src/shared/stacks/manifest-commands.ts +118 -0
  191. package/src/shared/stacks/render.ts +253 -0
  192. package/src/shared/stacks/runtime.ts +334 -0
  193. package/src/shared/stacks/status.ts +99 -0
  194. package/src/shared/stacks/types.ts +53 -0
  195. package/src/shared/terminal/tabs.ts +212 -0
  196. package/src/shared/test-discovery/module-scanner.ts +569 -0
  197. package/src/shared/types/index.ts +701 -0
  198. package/src/shared/ui-marker.ts +19 -0
  199. package/src/ui/cli/applications-command.ts +148 -0
  200. package/src/ui/cli/config-command.ts +68 -0
  201. package/src/ui/cli/create-main.ts +9 -0
  202. package/src/ui/cli/create.ts +360 -0
  203. package/src/ui/cli/edge-command.ts +81 -0
  204. package/src/ui/cli/flag-parser.ts +246 -0
  205. package/src/ui/cli/graph-command.ts +570 -0
  206. package/src/ui/cli/health-display.ts +63 -0
  207. package/src/ui/cli/index.ts +3742 -0
  208. package/src/ui/cli/init.ts +855 -0
  209. package/src/ui/cli/invocation-target.ts +175 -0
  210. package/src/ui/cli/logs-command.ts +104 -0
  211. package/src/ui/cli/mcp-main.ts +13 -0
  212. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  213. package/src/ui/cli/module-selection.ts +31 -0
  214. package/src/ui/cli/prompts.ts +767 -0
  215. package/src/ui/cli/results-viewer.ts +436 -0
  216. package/src/ui/cli/secrets-preflight.ts +91 -0
  217. package/src/ui/cli/stack-command.ts +117 -0
  218. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  219. package/src/ui/cli/test-runner.ts +1319 -0
  220. package/src/ui/cli/web-command.ts +1073 -0
  221. package/src/ui/mcp/api.ts +284 -0
  222. package/src/ui/mcp/guards.ts +141 -0
  223. package/src/ui/mcp/server.ts +147 -0
  224. package/src/ui/mcp/tools.ts +565 -0
  225. package/src/ui/server/audit.ts +71 -0
  226. package/src/ui/server/changelog-parser.ts +105 -0
  227. package/src/ui/server/daemon-reaper.ts +129 -0
  228. package/src/ui/server/editor-manager.ts +301 -0
  229. package/src/ui/server/github-service.ts +223 -0
  230. package/src/ui/server/graph-prefs.ts +89 -0
  231. package/src/ui/server/health-row.ts +15 -0
  232. package/src/ui/server/http-helpers.ts +98 -0
  233. package/src/ui/server/index.ts +3350 -0
  234. package/src/ui/server/log-history.ts +453 -0
  235. package/src/ui/server/perf-report.ts +42 -0
  236. package/src/ui/server/public/environments.html +496 -0
  237. package/src/ui/server/public/favicon.ico +0 -0
  238. package/src/ui/server/public/graph-model.js +416 -0
  239. package/src/ui/server/public/graph.css +976 -0
  240. package/src/ui/server/public/graph.js +986 -0
  241. package/src/ui/server/public/header.js +1373 -0
  242. package/src/ui/server/public/index.html +2595 -0
  243. package/src/ui/server/public/local-dev.html +5344 -0
  244. package/src/ui/server/public/logs.html +1496 -0
  245. package/src/ui/server/public/panel.js +787 -0
  246. package/src/ui/server/public/perf-report.js +359 -0
  247. package/src/ui/server/public/release.html +2111 -0
  248. package/src/ui/server/public/services.html +1103 -0
  249. package/src/ui/server/public/workspace-picker.js +69 -0
  250. package/src/ui/server/readonly-routes.ts +200 -0
  251. package/src/ui/server/release-deploy-session.ts +246 -0
  252. package/src/ui/server/release-service.ts +1125 -0
  253. package/src/ui/server/sudo-store.ts +111 -0
  254. package/src/ui/server/ui-assets.ts +73 -0
  255. package/src/ui/server/workspace-routes.ts +2185 -0
@@ -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();
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Result Reader
3
+ * Reads and queries test results from JSONL files
4
+ */
5
+
6
+ import { readFile, readdir } from "fs/promises";
7
+ import { resolve, join } from "path";
8
+ import type {
9
+ TestResult,
10
+ TestSummary,
11
+ FlakyTest,
12
+ TrendData,
13
+ Pillar,
14
+ } from "../types/index.js";
15
+
16
+ export interface QueryFilters {
17
+ pillar?: Pillar;
18
+ environment?: string;
19
+ dateFrom?: Date;
20
+ dateTo?: Date;
21
+ status?: "passed" | "failed" | "skipped";
22
+ user?: string;
23
+ }
24
+
25
+ export class ResultReader {
26
+ private resultsDir: string;
27
+
28
+ constructor(resultsDir: string = "./results") {
29
+ this.resultsDir = resolve(resultsDir);
30
+ }
31
+
32
+ /**
33
+ * Query results with filters
34
+ */
35
+ async query(filters: QueryFilters = {}): Promise<TestResult[]> {
36
+ const files = await this.getRelevantFiles(filters);
37
+ const results: TestResult[] = [];
38
+
39
+ for (const file of files) {
40
+ try {
41
+ const fileResults = await this.readJSONLFile(file);
42
+ const filtered = fileResults.filter((r) =>
43
+ this.matchesFilters(r, filters),
44
+ );
45
+ results.push(...filtered);
46
+ } catch (error) {
47
+ console.warn(
48
+ `Failed to read file ${file}: ${(error as Error).message}`,
49
+ );
50
+ }
51
+ }
52
+
53
+ // Sort by timestamp (newest first)
54
+ return results.sort(
55
+ (a, b) =>
56
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Get summary statistics for a pillar/environment
62
+ */
63
+ async getSummary(
64
+ pillar: Pillar,
65
+ environment: string,
66
+ days: number = 7,
67
+ ): Promise<TestSummary> {
68
+ const dateFrom = new Date();
69
+ dateFrom.setDate(dateFrom.getDate() - days);
70
+
71
+ const results = await this.query({ pillar, environment, dateFrom });
72
+
73
+ const total = results.length;
74
+ const passed = results.filter((r) => r.status === "passed").length;
75
+ const failed = results.filter((r) => r.status === "failed").length;
76
+ const skipped = results.filter((r) => r.status === "skipped").length;
77
+ const duration = results.reduce((sum, r) => sum + r.duration, 0);
78
+ const passRate = total > 0 ? (passed / total) * 100 : 0;
79
+
80
+ return {
81
+ pillar,
82
+ environment,
83
+ total,
84
+ passed,
85
+ failed,
86
+ skipped,
87
+ duration,
88
+ passRate,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Find flaky tests (tests that fail inconsistently)
94
+ */
95
+ async getFlakyTests(pillar: Pillar, days: number = 30): Promise<FlakyTest[]> {
96
+ const dateFrom = new Date();
97
+ dateFrom.setDate(dateFrom.getDate() - days);
98
+
99
+ const results = await this.query({ pillar, dateFrom });
100
+
101
+ // Group results by test name
102
+ const testGroups = results.reduce(
103
+ (acc, r) => {
104
+ if (!acc[r.test]) {
105
+ acc[r.test] = [];
106
+ }
107
+ acc[r.test].push(r);
108
+ return acc;
109
+ },
110
+ {} as Record<string, TestResult[]>,
111
+ );
112
+
113
+ // Find tests with both passes and failures
114
+ const flakyTests: FlakyTest[] = [];
115
+
116
+ for (const [testName, testResults] of Object.entries(testGroups)) {
117
+ const runs = testResults.length;
118
+ const failures = testResults.filter((r) => r.status === "failed").length;
119
+
120
+ // Consider a test flaky if it has both passes and failures
121
+ if (failures > 0 && failures < runs && runs >= 3) {
122
+ flakyTests.push({
123
+ test: testName,
124
+ runs,
125
+ failures,
126
+ flakyRate: (failures / runs) * 100,
127
+ });
128
+ }
129
+ }
130
+
131
+ // Sort by flaky rate (most flaky first)
132
+ return flakyTests.sort((a, b) => b.flakyRate - a.flakyRate);
133
+ }
134
+
135
+ /**
136
+ * Get trend data for visualization
137
+ */
138
+ async getTrends(
139
+ pillar: Pillar,
140
+ environment: string,
141
+ days: number = 30,
142
+ ): Promise<TrendData[]> {
143
+ const dateFrom = new Date();
144
+ dateFrom.setDate(dateFrom.getDate() - days);
145
+
146
+ const results = await this.query({ pillar, environment, dateFrom });
147
+
148
+ // Group results by date
149
+ const dateGroups = results.reduce(
150
+ (acc, r) => {
151
+ const date = r.timestamp.split("T")[0];
152
+ if (!acc[date]) {
153
+ acc[date] = [];
154
+ }
155
+ acc[date].push(r);
156
+ return acc;
157
+ },
158
+ {} as Record<string, TestResult[]>,
159
+ );
160
+
161
+ // Calculate metrics for each date
162
+ const trends: TrendData[] = [];
163
+
164
+ for (const [date, dayResults] of Object.entries(dateGroups)) {
165
+ const total = dayResults.length;
166
+ const passed = dayResults.filter((r) => r.status === "passed").length;
167
+ const passRate = total > 0 ? (passed / total) * 100 : 0;
168
+ const avgDuration =
169
+ total > 0
170
+ ? dayResults.reduce((sum, r) => sum + r.duration, 0) / total
171
+ : 0;
172
+
173
+ trends.push({
174
+ date,
175
+ passRate,
176
+ avgDuration,
177
+ totalTests: total,
178
+ });
179
+ }
180
+
181
+ // Sort by date (oldest first)
182
+ return trends.sort((a, b) => a.date.localeCompare(b.date));
183
+ }
184
+
185
+ /**
186
+ * Read JSONL file and parse results
187
+ */
188
+ private async readJSONLFile(filepath: string): Promise<TestResult[]> {
189
+ const content = await readFile(filepath, "utf-8");
190
+ const lines = content
191
+ .trim()
192
+ .split("\n")
193
+ .filter((l) => l.length > 0);
194
+
195
+ return lines
196
+ .map((line) => {
197
+ try {
198
+ return JSON.parse(line) as TestResult;
199
+ } catch (error) {
200
+ console.warn(`Failed to parse line in ${filepath}: ${line}`);
201
+ return null;
202
+ }
203
+ })
204
+ .filter((r): r is TestResult => r !== null);
205
+ }
206
+
207
+ /**
208
+ * Get relevant files based on filters
209
+ */
210
+ private async getRelevantFiles(filters: QueryFilters): Promise<string[]> {
211
+ try {
212
+ const files = await readdir(this.resultsDir);
213
+
214
+ return files
215
+ .filter((f) => f.endsWith(".jsonl"))
216
+ .filter((f) => {
217
+ // Filter by pillar if specified
218
+ if (filters.pillar && !f.startsWith(filters.pillar)) {
219
+ return false;
220
+ }
221
+
222
+ // Filter by environment if specified
223
+ if (filters.environment && !f.includes(`-${filters.environment}-`)) {
224
+ return false;
225
+ }
226
+
227
+ return true;
228
+ })
229
+ .map((f) => join(this.resultsDir, f));
230
+ } catch {
231
+ return [];
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Check if result matches filters
237
+ */
238
+ private matchesFilters(result: TestResult, filters: QueryFilters): boolean {
239
+ if (filters.status && result.status !== filters.status) {
240
+ return false;
241
+ }
242
+
243
+ if (filters.user && result.user !== filters.user) {
244
+ return false;
245
+ }
246
+
247
+ if (filters.dateFrom) {
248
+ const resultDate = new Date(result.timestamp);
249
+ if (resultDate < filters.dateFrom) {
250
+ return false;
251
+ }
252
+ }
253
+
254
+ if (filters.dateTo) {
255
+ const resultDate = new Date(result.timestamp);
256
+ if (resultDate > filters.dateTo) {
257
+ return false;
258
+ }
259
+ }
260
+
261
+ return true;
262
+ }
263
+ }
264
+
265
+ // Export singleton instance
266
+ export const resultReader = new ResultReader();