@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,140 @@
1
+ /**
2
+ * Global Setup for Staged Synthetic Tests
3
+ *
4
+ * 1. Verify web host is reachable (fail fast if app is down)
5
+ * 2. Log in as admin + user in parallel via headless browser login
6
+ * — credentials from the secrets backend (local) or ENV vars (CI)
7
+ * — sessions are ephemeral: written fresh each run, never manually managed
8
+ */
9
+
10
+ import { type FullConfig } from "@playwright/test";
11
+ import * as path from "path";
12
+ import * as fs from "fs";
13
+ import {
14
+ getCredentials,
15
+ headlessLogin,
16
+ } from "../../shared/auth/headless-login.js";
17
+ import { envSelector } from "../../shared/environment/env-selector.js";
18
+ import type {
19
+ BrowserLoginConfig,
20
+ Environment,
21
+ } from "../../shared/types/index.js";
22
+ import { getDistRoot } from "../../shared/paths.js";
23
+
24
+ import { product } from "../../shared/product.js";
25
+ // Den-root anchored, NOT process.cwd(): auth sessions are WRITTEN here and
26
+ // the consuming configs read storageState from the anchored resolveAuthPath.
27
+ const AUTH_DIR = path.resolve(getDistRoot(), "playwright/.auth");
28
+
29
+ async function login(
30
+ role: "admin" | "user" | "beta",
31
+ baseUrl: string,
32
+ environment: Environment,
33
+ browserLogin?: BrowserLoginConfig,
34
+ ): Promise<void> {
35
+ const authFile = path.join(AUTH_DIR, `${role}-${environment}.json`);
36
+ const creds = await getCredentials(role, environment);
37
+ if (!creds) {
38
+ const localHint =
39
+ role === "admin"
40
+ ? ` Local: make auth-configure env=${environment} (sets up your admin account)\n`
41
+ : ` Local: sign in to the secrets backend; the ${role} account comes from test_accounts in the org config\n`;
42
+ throw new Error(
43
+ `No credentials for ${role}.\n` +
44
+ localHint +
45
+ ` CI: set E2E_${role.toUpperCase()}_EMAIL / E2E_${role.toUpperCase()}_PASSWORD`,
46
+ );
47
+ }
48
+ await headlessLogin(
49
+ role,
50
+ baseUrl,
51
+ creds.email,
52
+ creds.password,
53
+ authFile,
54
+ browserLogin,
55
+ );
56
+ }
57
+
58
+ async function globalSetup(_config: FullConfig) {
59
+ const environment = (process.env.E2E_ENVIRONMENT as Environment) || "local";
60
+ const envConfig = await envSelector.loadEnvironment(environment);
61
+ const baseUrl = envConfig.baseUrls.web;
62
+ const browserLogin = envConfig.auth?.browser;
63
+
64
+ // ─── Health Check ──────────────────────────────────────────────────────────
65
+ console.log(`\n🏥 Health check: ${baseUrl}\n`);
66
+ try {
67
+ const response = await fetch(baseUrl, {
68
+ method: "GET",
69
+ signal: AbortSignal.timeout(5000),
70
+ });
71
+
72
+ if (!response.ok && response.status !== 401 && response.status !== 403) {
73
+ console.error(`❌ Host returned error status: ${response.status}`);
74
+ console.error(` URL: ${baseUrl}`);
75
+ console.error(
76
+ `\n💡 Make sure the app is running before running tests.\n`,
77
+ );
78
+ process.exit(1);
79
+ }
80
+
81
+ console.log(`✅ Host is reachable (status: ${response.status})\n`);
82
+ } catch (error: unknown) {
83
+ const isLocalUrl = baseUrl.startsWith("http://");
84
+
85
+ console.error(`❌ Cannot reach host: ${baseUrl}`);
86
+ console.error(` Error: ${(error as Error).message}`);
87
+ if (isLocalUrl) {
88
+ console.error(`\n💡 The app does not appear to be running.`);
89
+ console.error(
90
+ ` Start it first (for example: ${product().cli} up, or the app repo's own make up).\n`,
91
+ );
92
+ } else {
93
+ console.error(`\n💡 Cannot reach ${baseUrl}. Check your network.\n`);
94
+ }
95
+ process.exit(1);
96
+ }
97
+
98
+ // ─── Auth: parallel headless login ─────────────────────────────────────────
99
+ console.log("🔐 Authenticating (admin + user + beta in parallel)...\n");
100
+
101
+ if (!fs.existsSync(AUTH_DIR)) {
102
+ fs.mkdirSync(AUTH_DIR, { recursive: true });
103
+ }
104
+
105
+ // admin + user are required — any failure aborts the run.
106
+ // beta is optional — account may not be seeded in all envs yet;
107
+ // a failure here warns and skips, letting beta tests fail on their own.
108
+ try {
109
+ await Promise.all([
110
+ login("admin", baseUrl, environment, browserLogin),
111
+ login("user", baseUrl, environment, browserLogin),
112
+ ]);
113
+ } catch (err: unknown) {
114
+ console.error(`\n❌ Authentication failed: ${(err as Error).message}\n`);
115
+ process.exit(1);
116
+ }
117
+
118
+ try {
119
+ await login("beta", baseUrl, environment, browserLogin);
120
+ } catch (err: unknown) {
121
+ console.warn(`\n⚠️ Beta login skipped: ${(err as Error).message}`);
122
+ console.warn(
123
+ ` Ensure the beta test account exists for ${environment}.\n`,
124
+ );
125
+ }
126
+
127
+ // Create bare symlinks (admin.json, user.json, beta.json) pointing to the
128
+ // current env's auth files. Some spec files in linked repos hardcode the
129
+ // non-suffixed path; these symlinks keep them working without code changes.
130
+ for (const role of ["admin", "user", "beta"]) {
131
+ const bare = path.join(AUTH_DIR, `${role}.json`);
132
+ const target = `${role}-${environment}.json`;
133
+ if (fs.existsSync(bare)) fs.unlinkSync(bare);
134
+ fs.symlinkSync(target, bare);
135
+ }
136
+
137
+ console.log("\n✅ Pre-flight checks complete\n");
138
+ }
139
+
140
+ export default globalSetup;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Global Teardown for Staged Synthetic Tests
3
+ * Logs screenshot count retained in test-results/.
4
+ */
5
+
6
+ import { type FullConfig } from "@playwright/test";
7
+ import * as path from "path";
8
+ import * as fs from "fs/promises";
9
+ import { getDistRoot } from "../../shared/paths.js";
10
+
11
+ async function globalTeardown(_config: FullConfig) {
12
+ console.log("\n📸 Ensuring screenshots are retained...");
13
+
14
+ // read-only screenshot count; anchored so it matches the config outputDir
15
+ const testResultsDir = path.join(getDistRoot(), "test-results");
16
+
17
+ try {
18
+ const exists = await fs
19
+ .access(testResultsDir)
20
+ .then(() => true)
21
+ .catch(() => false);
22
+
23
+ if (exists) {
24
+ const entries = await fs.readdir(testResultsDir, { withFileTypes: true });
25
+ let screenshotCount = 0;
26
+
27
+ for (const entry of entries) {
28
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
29
+ const testDir = path.join(testResultsDir, entry.name);
30
+ const files = await fs.readdir(testDir);
31
+ screenshotCount += files.filter((f) => f.endsWith(".png")).length;
32
+ }
33
+ }
34
+
35
+ console.log(
36
+ `✓ ${screenshotCount} screenshots retained in test-results/\n`,
37
+ );
38
+ } else {
39
+ console.log("⚠️ No test-results directory found\n");
40
+ }
41
+ } catch (error) {
42
+ console.warn(
43
+ `Warning: Could not check screenshots: ${(error as Error).message}\n`,
44
+ );
45
+ }
46
+ }
47
+
48
+ export default globalTeardown;
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Playwright Configuration for Staged Synthetic Tests
3
+ *
4
+ * Runs test files from .synth-run/<repo>/ (staged by pillars/browser/setup.ts).
5
+ * - chromium regular tests (no saved auth)
6
+ * - chromium-admin admin-named tests (saved admin session)
7
+ * - chromium-user user-named tests (saved user session)
8
+ * - firefox cross-browser (no saved auth)
9
+ * - webkit cross-browser (no saved auth)
10
+ * - chromium-last *.last.spec.ts tests, run LAST (via dependencies) —
11
+ * for suites that change shared state other tests read
12
+ */
13
+
14
+ import { defineConfig, devices } from "@playwright/test";
15
+ import type {
16
+ Reporter,
17
+ TestCase,
18
+ TestResult as PlaywrightTestResult,
19
+ } from "@playwright/test/reporter";
20
+ import * as fs from "fs";
21
+ import * as path from "path";
22
+ import { getDistRoot } from "../../shared/paths.js";
23
+ import { distPath } from "../../shared/engine.js";
24
+ import { fileURLToPath } from "url";
25
+ import { resultWriter } from "../../shared/results/result-writer.js";
26
+ import {
27
+ resolveAuthPath,
28
+ resolveEnv,
29
+ } from "../../shared/environment/playwright-env.js";
30
+ import * as os from "os";
31
+
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = path.dirname(__filename);
34
+
35
+ // Auto-detect: SYNTH_REPO env, or the first directory in .synth-run/
36
+ const repoName = (() => {
37
+ if (process.env.SYNTH_REPO) return process.env.SYNTH_REPO;
38
+ const synthRunDir = distPath(".synth-run");
39
+ try {
40
+ const entries = fs.readdirSync(synthRunDir, { withFileTypes: true });
41
+ const dir = entries.find((e) => e.isDirectory());
42
+ if (dir) return dir.name;
43
+ } catch {
44
+ /* .synth-run doesn't exist yet */
45
+ }
46
+ throw new Error(
47
+ "SYNTH_REPO is not set and nothing is staged in .synth-run/ — run the " +
48
+ "synthetic setup for a repo first (make test-syn repo=<name>).",
49
+ );
50
+ })();
51
+ const env = resolveEnv("local");
52
+ // The distribution root: setup.ts stages .synth-run there; auth lives there.
53
+ const repoRoot = getDistRoot();
54
+
55
+ const adminAuthPath = resolveAuthPath("admin", env, repoRoot);
56
+ const userAuthPath = resolveAuthPath("user", env, repoRoot);
57
+ const betaAuthPath = resolveAuthPath("beta", env, repoRoot);
58
+
59
+ // repoRoot-anchored in lockstep with setup.ts (the .synth-run writer).
60
+ const stagingDir = path.resolve(repoRoot, `.synth-run/${repoName}`);
61
+
62
+ // Specs that must run after every other project (they change shared state).
63
+ const LAST_SPECS = /.*\.last\.spec\.ts$/;
64
+
65
+ // Inline JSONL Reporter
66
+ class JSONLReporter implements Reporter {
67
+ async onTestEnd(test: TestCase, result: PlaywrightTestResult) {
68
+ try {
69
+ const environment = (process.env.E2E_ENVIRONMENT || "local") as any;
70
+ const testTitle = test.titlePath().slice(1).join(" › ");
71
+ const testFile =
72
+ test.location.file
73
+ .split(`/.synth-run/${repoName}/`)[1]
74
+ ?.replace(/\.spec\.ts$/, "") ||
75
+ path.basename(test.location.file, ".spec.ts");
76
+
77
+ let status: "passed" | "failed" | "skipped";
78
+ if (result.status === "passed") status = "passed";
79
+ else if (result.status === "skipped") status = "skipped";
80
+ else status = "failed";
81
+
82
+ const screenshot = result.attachments.find(
83
+ (a) => a.name === "screenshot" || a.contentType?.startsWith("image/"),
84
+ )?.path;
85
+ const trace = result.attachments.find(
86
+ (a) => a.name === "trace" || a.contentType?.includes("zip"),
87
+ )?.path;
88
+
89
+ await resultWriter.writeResult({
90
+ timestamp: new Date().toISOString(),
91
+ pillar: "synthetic",
92
+ environment,
93
+ test: `${testFile} › ${testTitle}`,
94
+ status,
95
+ duration: result.duration,
96
+ user: process.env.USER || os.userInfo().username || "unknown",
97
+ error: result.error?.message,
98
+ screenshot,
99
+ trace,
100
+ metadata: {
101
+ browser: test.parent.project()?.name,
102
+ retries: result.retry,
103
+ },
104
+ });
105
+ } catch (error) {
106
+ console.warn(
107
+ `[JSONL Reporter] Failed to write result: ${(error as Error).message}`,
108
+ );
109
+ }
110
+ }
111
+ }
112
+
113
+ export default defineConfig({
114
+ testDir: stagingDir,
115
+
116
+ globalSetup: path.resolve(__dirname, "./global-setup.ts"),
117
+ globalTeardown: path.resolve(__dirname, "./global-teardown.ts"),
118
+
119
+ fullyParallel: true,
120
+ forbidOnly: !!process.env.CI,
121
+ retries: process.env.CI ? 2 : 0,
122
+ workers: process.env.CI ? 1 : undefined,
123
+
124
+ reporter: [
125
+ ["html", { outputFolder: distPath("playwright-report") }],
126
+ ["list"],
127
+ ],
128
+
129
+ use: {
130
+ baseURL: process.env.BASE_URL || "http://localhost:3000",
131
+ trace: "retain-on-failure",
132
+ screenshot: "on",
133
+ video: "retain-on-failure",
134
+ actionTimeout: 10000,
135
+ navigationTimeout: 60000,
136
+ },
137
+
138
+ projects: [
139
+ // Regular tests (no saved auth)
140
+ {
141
+ name: "chromium",
142
+ use: { ...devices["Desktop Chrome"] },
143
+ testIgnore: [
144
+ /.*user.*\.spec\.ts$/,
145
+ /.*admin.*\.spec\.ts$/,
146
+ /.*beta.*\.spec\.ts$/,
147
+ LAST_SPECS,
148
+ ],
149
+ },
150
+
151
+ // Admin tests (saved admin session)
152
+ {
153
+ name: "chromium-admin",
154
+ use: {
155
+ ...devices["Desktop Chrome"],
156
+ storageState: adminAuthPath,
157
+ },
158
+ testMatch: /.*admin.*.spec.ts/,
159
+ },
160
+
161
+ // User tests (saved user session)
162
+ {
163
+ name: "chromium-user",
164
+ use: {
165
+ ...devices["Desktop Chrome"],
166
+ storageState: userAuthPath,
167
+ },
168
+ testMatch: /.*user.*\.spec\.ts$/,
169
+ },
170
+
171
+ // Beta tests (saved beta session) — restricted-access user
172
+ {
173
+ name: "chromium-beta",
174
+ use: {
175
+ ...devices["Desktop Chrome"],
176
+ storageState: betaAuthPath,
177
+ },
178
+ testMatch: /.*beta.*\.spec\.ts$/,
179
+ },
180
+
181
+ {
182
+ name: "firefox",
183
+ use: { ...devices["Desktop Firefox"] },
184
+ testIgnore: [LAST_SPECS, /.*beta.*\.spec\.ts$/],
185
+ },
186
+
187
+ {
188
+ name: "webkit",
189
+ use: { ...devices["Desktop Safari"] },
190
+ testIgnore: [LAST_SPECS, /.*beta.*\.spec\.ts$/],
191
+ },
192
+
193
+ // *.last.spec.ts — runs AFTER all others complete
194
+ {
195
+ name: "chromium-last",
196
+ use: { ...devices["Desktop Chrome"] },
197
+ testMatch: LAST_SPECS,
198
+ dependencies: [
199
+ "chromium",
200
+ "chromium-admin",
201
+ "chromium-user",
202
+ "chromium-beta",
203
+ "firefox",
204
+ "webkit",
205
+ ],
206
+ },
207
+ ],
208
+
209
+ timeout: 60000,
210
+ expect: { timeout: 10000 },
211
+ outputDir: distPath("test-results"),
212
+ });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Synthetic Test Staging Setup
3
+ *
4
+ * Creates .synth-run/{repo}/ before each test run:
5
+ *
6
+ * 1. Copies all *.spec.ts files + fixture assets from the repo's testDir
7
+ * 2. Creates node_modules/@playwright/test → den's playwright
8
+ * 3. Creates node_modules/@den/test → den's pillars/browser/fixtures/
9
+ *
10
+ * Why: service repos usually have their own @playwright/test in
11
+ * node_modules. If Playwright's test runner loads test files from those
12
+ * directories, Node resolves @playwright/test to the repo's copy — which is
13
+ * a different module instance than the runner's, causing the "Requiring
14
+ * @playwright/test second time" error.
15
+ *
16
+ * Staging the files inside den (with a node_modules shim) ensures
17
+ * there is exactly one @playwright/test instance: den's.
18
+ *
19
+ * The @den/test shim lets test files import from `@den/test`
20
+ * instead of fragile relative paths like `../../fixtures/index.js`.
21
+ *
22
+ * The staged dir is gitignored (.synth-run/) and re-created fresh each run.
23
+ */
24
+
25
+ import * as fs from "fs";
26
+ import * as path from "path";
27
+ import {
28
+ discoverRepos,
29
+ findManifest,
30
+ resolveRepoPath,
31
+ } from "../smoke/discover.js";
32
+ import type { Environment } from "../../shared/types/index.js";
33
+ import { getDistRoot } from "../../shared/paths.js";
34
+
35
+ import { product } from "../../shared/product.js";
36
+ import { enginePath } from "../../shared/engine.js";
37
+ // Den-root anchored, NOT process.cwd(): stages (rm -rf + mkdir + symlink
38
+ // + copy) .synth-run, and CLI-spawned runs inherit the user's cwd.
39
+ const PROJECT_ROOT = getDistRoot();
40
+
41
+ const SYNTH_RUN_DIR = path.resolve(PROJECT_ROOT, ".synth-run");
42
+ const ENGINE_MODULES = path.resolve(PROJECT_ROOT, "node_modules");
43
+ const SYNTHETIC_FIXTURES = enginePath("src/pillars/browser/fixtures");
44
+ interface SyntheticsManifest {
45
+ testDir: string;
46
+ testMatch: string;
47
+ baseUrlKey: "api" | "web";
48
+ auth: ("admin" | "user")[];
49
+ }
50
+
51
+ interface SyntheticsManifest {
52
+ name?: string;
53
+ synthetics?: SyntheticsManifest;
54
+ }
55
+
56
+ function findSyntheticsManifest(
57
+ repoName: string,
58
+ ): { repoPath: string; synthetics: SyntheticsManifest; name: string } | null {
59
+ // Use workspace-aware discovery (active workspace when Local Dev is enabled,
60
+ // registered-repos.json + .den/ symlinks otherwise).
61
+ const discovered = findManifest(repoName);
62
+ if (!discovered) return null;
63
+
64
+ const manifest = discovered.manifest as SyntheticsManifest | null;
65
+ if (!manifest?.synthetics) return null;
66
+ return {
67
+ repoPath: discovered.repoPath,
68
+ synthetics: manifest.synthetics,
69
+ name: manifest.name ?? repoName,
70
+ };
71
+ }
72
+
73
+ // SPEC_RE matches .spec.ts files but not .spec.ts.skip files
74
+ const SPEC_RE = /\.spec\.ts$/;
75
+
76
+ // Asset extensions that should be copied alongside spec files (fixtures)
77
+ const ASSET_RE =
78
+ /\.(png|jpg|jpeg|gif|webp|pdf|glb|npy|zip|json|txt|csv|mp4|mp3|wav|svg)$/i;
79
+
80
+ function copyDir(src: string, dst: string): number {
81
+ let count = 0;
82
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
83
+ if (entry.name.startsWith(".")) continue;
84
+
85
+ if (entry.isDirectory()) {
86
+ const subDst = path.join(dst, entry.name);
87
+ fs.mkdirSync(subDst, { recursive: true });
88
+ count += copyDir(path.join(src, entry.name), subDst);
89
+ } else if (
90
+ SPEC_RE.test(entry.name) ||
91
+ entry.name === "manifest.yml" ||
92
+ ASSET_RE.test(entry.name)
93
+ ) {
94
+ fs.copyFileSync(path.join(src, entry.name), path.join(dst, entry.name));
95
+ count++;
96
+ }
97
+ }
98
+ return count;
99
+ }
100
+
101
+ /**
102
+ * Auto-detect which discovered repo has a synthetics section with a valid
103
+ * testDir. Returns the first match, or null if none found.
104
+ */
105
+ function autoDetectSyntheticsRepo(): {
106
+ repoPath: string;
107
+ synthetics: SyntheticsManifest;
108
+ name: string;
109
+ } | null {
110
+ for (const repo of discoverRepos()) {
111
+ const manifest = repo.manifest as SyntheticsManifest | null;
112
+ if (!manifest?.synthetics) continue;
113
+ const testDir = path.resolve(repo.repoPath, manifest.synthetics.testDir);
114
+ if (fs.existsSync(testDir)) {
115
+ return {
116
+ repoPath: repo.repoPath,
117
+ synthetics: manifest.synthetics,
118
+ name: manifest.name ?? repo.repoName,
119
+ };
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+
125
+ async function main() {
126
+ const explicitRepo = process.env.SYNTH_REPO;
127
+ const environment = (process.env.E2E_ENVIRONMENT || "local") as Environment;
128
+
129
+ // If SYNTH_REPO is set, look up that specific repo. Otherwise auto-detect
130
+ // from the active workspace (or registered repos) by finding the first repo
131
+ // whose synthetics.testDir actually exists on disk.
132
+ const discovered = explicitRepo
133
+ ? findSyntheticsManifest(explicitRepo)
134
+ : autoDetectSyntheticsRepo();
135
+
136
+ if (!discovered) {
137
+ const hint = explicitRepo
138
+ ? `repo "${explicitRepo}"`
139
+ : "any discovered repo";
140
+ console.error(`❌ No synthetics config found in ${hint}.`);
141
+ console.error(
142
+ ` Ensure the repo is in your active workspace or registered via make register-repos,`,
143
+ );
144
+ console.error(
145
+ ` and that its ${product().manifestYml} contains a "synthetics" section with a valid testDir.`,
146
+ );
147
+ process.exit(1);
148
+ }
149
+
150
+ const repoName = discovered.name;
151
+
152
+ // For non-local environments, read test files from the SHA-pinned clone
153
+ // so tests always match the deployed code (no drift).
154
+ // setup-env.ts must have been run first to create the clone.
155
+ let repoPath = discovered.repoPath;
156
+ if (environment !== "local") {
157
+ const repo = findManifest(repoName);
158
+ if (repo) {
159
+ try {
160
+ repoPath = resolveRepoPath(repo, environment);
161
+ } catch (err) {
162
+ console.error(`❌ ${(err as Error).message}`);
163
+ process.exit(1);
164
+ }
165
+ }
166
+ }
167
+
168
+ const { synthetics } = discovered;
169
+ const testDir = path.resolve(repoPath, synthetics.testDir);
170
+
171
+ if (!fs.existsSync(testDir)) {
172
+ console.error(`❌ Synthetics testDir not found: ${testDir}`);
173
+ process.exit(1);
174
+ }
175
+
176
+ const stagingDir = path.join(SYNTH_RUN_DIR, repoName);
177
+
178
+ // Clean the entire .synth-run/ dir (not just this repo's subdir) so that
179
+ // stale dirs from a previous repo don't confuse auto-detection downstream.
180
+ fs.rmSync(SYNTH_RUN_DIR, { recursive: true, force: true });
181
+ fs.mkdirSync(stagingDir, { recursive: true });
182
+
183
+ // ── Node modules shim ──────────────────────────────────────────────────────
184
+ // @playwright/test → den's playwright (prevents "second time" error)
185
+ // @den/test → den's pillars/browser/fixtures/ (clean fixture import)
186
+ const nmDir = path.join(stagingDir, "node_modules");
187
+ const pwScopeDir = path.join(nmDir, "@playwright");
188
+ const denScopeDir = path.join(nmDir, `@${product().cli}`);
189
+ fs.mkdirSync(pwScopeDir, { recursive: true });
190
+ fs.mkdirSync(denScopeDir, { recursive: true });
191
+
192
+ const pwTestSrc = path.join(ENGINE_MODULES, "@playwright", "test");
193
+ const pwTestDst = path.join(pwScopeDir, "test");
194
+ fs.symlinkSync(pwTestSrc, pwTestDst);
195
+
196
+ const pwSrc = path.join(ENGINE_MODULES, "playwright");
197
+ const pwDst = path.join(nmDir, "playwright");
198
+ fs.symlinkSync(pwSrc, pwDst);
199
+
200
+ // @den/test — current import name
201
+ const denTestDst = path.join(denScopeDir, "test");
202
+ fs.symlinkSync(SYNTHETIC_FIXTURES, denTestDst);
203
+
204
+ // ── Copy test files + assets ───────────────────────────────────────────────
205
+ const count = copyDir(testDir, stagingDir);
206
+ if (count === 0) {
207
+ console.warn(` ⚠ No .spec.ts or asset files found in ${testDir}`);
208
+ }
209
+
210
+ console.log(`✅ Staged ${count} file(s) → .synth-run/${repoName}/`);
211
+ console.log(
212
+ ` Shims: @playwright/test → ${product().cli}/node_modules, @${product().cli}/test → pillars/browser/fixtures/`,
213
+ );
214
+ }
215
+
216
+ main().catch((err) => {
217
+ console.error(err);
218
+ process.exit(1);
219
+ });