@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.
- package/CHANGELOG.md +204 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- package/bin/den +68 -0
- package/docs/distributions.md +499 -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 +101 -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 +403 -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 +124 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +757 -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/agent-actor.ts +77 -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 +688 -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/status.ts +74 -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 +635 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +108 -0
- package/src/shared/environment-manager/log-tail.ts +213 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +335 -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-container.ts +47 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1603 -0
- package/src/shared/environment-manager/sync-targets.ts +66 -0
- package/src/shared/environment-manager/up-plan.ts +326 -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/merge-override.ts +75 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/placement.ts +321 -0
- package/src/shared/manifest/read-manifest.ts +278 -0
- package/src/shared/manifest/schema.ts +313 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-layers.ts +229 -0
- package/src/shared/manifest/validate-manifest.ts +200 -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 +190 -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/redact.ts +107 -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 +118 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/status.ts +99 -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 +701 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +148 -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 +81 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +570 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3742 -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/mcp-main.ts +13 -0
- package/src/ui/cli/mcp-stdout-guard.ts +10 -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 +117 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1319 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/mcp/api.ts +284 -0
- package/src/ui/mcp/guards.ts +141 -0
- package/src/ui/mcp/server.ts +147 -0
- package/src/ui/mcp/tools.ts +565 -0
- package/src/ui/server/audit.ts +71 -0
- package/src/ui/server/changelog-parser.ts +105 -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/graph-prefs.ts +89 -0
- package/src/ui/server/health-row.ts +15 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3350 -0
- package/src/ui/server/log-history.ts +453 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +496 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/graph-model.js +416 -0
- package/src/ui/server/public/graph.css +976 -0
- package/src/ui/server/public/graph.js +986 -0
- package/src/ui/server/public/header.js +1373 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +5344 -0
- package/src/ui/server/public/logs.html +1496 -0
- package/src/ui/server/public/panel.js +787 -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 +1103 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/readonly-routes.ts +200 -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/ui-assets.ts +73 -0
- 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();
|