@alchemy.run/test 0.0.0 → 2.0.0-beta.85

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 (119) hide show
  1. package/LICENSE +201 -0
  2. package/bin/alchemy-test.ts +2 -0
  3. package/lib/Alchemy.d.ts +78 -0
  4. package/lib/Alchemy.d.ts.map +1 -0
  5. package/lib/Alchemy.js +194 -0
  6. package/lib/Alchemy.js.map +1 -0
  7. package/lib/Api.d.ts +134 -0
  8. package/lib/Api.d.ts.map +1 -0
  9. package/lib/Api.js +245 -0
  10. package/lib/Api.js.map +1 -0
  11. package/lib/Bun.d.ts +67 -0
  12. package/lib/Bun.d.ts.map +1 -0
  13. package/lib/Bun.js +143 -0
  14. package/lib/Bun.js.map +1 -0
  15. package/lib/Capture.d.ts +10 -0
  16. package/lib/Capture.d.ts.map +1 -0
  17. package/lib/Capture.js +68 -0
  18. package/lib/Capture.js.map +1 -0
  19. package/lib/Core.d.ts +287 -0
  20. package/lib/Core.d.ts.map +1 -0
  21. package/lib/Core.js +429 -0
  22. package/lib/Core.js.map +1 -0
  23. package/lib/DevCli.d.ts +47 -0
  24. package/lib/DevCli.d.ts.map +1 -0
  25. package/lib/DevCli.js +181 -0
  26. package/lib/DevCli.js.map +1 -0
  27. package/lib/Expect.d.ts +84 -0
  28. package/lib/Expect.d.ts.map +1 -0
  29. package/lib/Expect.js +518 -0
  30. package/lib/Expect.js.map +1 -0
  31. package/lib/FileLog.d.ts +27 -0
  32. package/lib/FileLog.d.ts.map +1 -0
  33. package/lib/FileLog.js +136 -0
  34. package/lib/FileLog.js.map +1 -0
  35. package/lib/Http.d.ts +109 -0
  36. package/lib/Http.d.ts.map +1 -0
  37. package/lib/Http.js +123 -0
  38. package/lib/Http.js.map +1 -0
  39. package/lib/Model.d.ts +98 -0
  40. package/lib/Model.d.ts.map +1 -0
  41. package/lib/Model.js +59 -0
  42. package/lib/Model.js.map +1 -0
  43. package/lib/PlainReporter.d.ts +19 -0
  44. package/lib/PlainReporter.d.ts.map +1 -0
  45. package/lib/PlainReporter.js +348 -0
  46. package/lib/PlainReporter.js.map +1 -0
  47. package/lib/Plan.d.ts +19 -0
  48. package/lib/Plan.d.ts.map +1 -0
  49. package/lib/Plan.js +32 -0
  50. package/lib/Plan.js.map +1 -0
  51. package/lib/Registry.d.ts +34 -0
  52. package/lib/Registry.d.ts.map +1 -0
  53. package/lib/Registry.js +80 -0
  54. package/lib/Registry.js.map +1 -0
  55. package/lib/Reporter.d.ts +160 -0
  56. package/lib/Reporter.d.ts.map +1 -0
  57. package/lib/Reporter.js +9 -0
  58. package/lib/Reporter.js.map +1 -0
  59. package/lib/Runner.d.ts +51 -0
  60. package/lib/Runner.d.ts.map +1 -0
  61. package/lib/Runner.js +1052 -0
  62. package/lib/Runner.js.map +1 -0
  63. package/lib/StrayOutput.d.ts +8 -0
  64. package/lib/StrayOutput.d.ts.map +1 -0
  65. package/lib/StrayOutput.js +259 -0
  66. package/lib/StrayOutput.js.map +1 -0
  67. package/lib/Tags.d.ts +17 -0
  68. package/lib/Tags.d.ts.map +1 -0
  69. package/lib/Tags.js +84 -0
  70. package/lib/Tags.js.map +1 -0
  71. package/lib/TestState.d.ts +12 -0
  72. package/lib/TestState.d.ts.map +1 -0
  73. package/lib/TestState.js +15 -0
  74. package/lib/TestState.js.map +1 -0
  75. package/lib/Tui.d.ts +4 -0
  76. package/lib/Tui.d.ts.map +1 -0
  77. package/lib/Tui.js +1137 -0
  78. package/lib/Tui.js.map +1 -0
  79. package/lib/Vitest.d.ts +58 -0
  80. package/lib/Vitest.d.ts.map +1 -0
  81. package/lib/Vitest.js +132 -0
  82. package/lib/Vitest.js.map +1 -0
  83. package/lib/WorktreeStage.d.ts +17 -0
  84. package/lib/WorktreeStage.d.ts.map +1 -0
  85. package/lib/WorktreeStage.js +48 -0
  86. package/lib/WorktreeStage.js.map +1 -0
  87. package/lib/cli.d.ts +2 -0
  88. package/lib/cli.d.ts.map +1 -0
  89. package/lib/cli.js +222 -0
  90. package/lib/cli.js.map +1 -0
  91. package/lib/index.d.ts +14 -0
  92. package/lib/index.d.ts.map +1 -0
  93. package/lib/index.js +12 -0
  94. package/lib/index.js.map +1 -0
  95. package/package.json +73 -3
  96. package/src/Alchemy.ts +335 -0
  97. package/src/Api.ts +521 -0
  98. package/src/Bun.ts +256 -0
  99. package/src/Capture.ts +72 -0
  100. package/src/Core.ts +872 -0
  101. package/src/DevCli.ts +229 -0
  102. package/src/Expect.ts +763 -0
  103. package/src/FileLog.ts +158 -0
  104. package/src/Http.ts +201 -0
  105. package/src/Model.ts +160 -0
  106. package/src/PlainReporter.ts +426 -0
  107. package/src/Plan.ts +45 -0
  108. package/src/Registry.ts +101 -0
  109. package/src/Reporter.ts +171 -0
  110. package/src/Runner.ts +1341 -0
  111. package/src/StrayOutput.ts +283 -0
  112. package/src/Tags.ts +100 -0
  113. package/src/TestState.ts +35 -0
  114. package/src/Tui.ts +1272 -0
  115. package/src/Vitest.ts +239 -0
  116. package/src/WorktreeStage.ts +45 -0
  117. package/src/cli.ts +330 -0
  118. package/src/index.ts +57 -0
  119. package/README.md +0 -9
package/lib/FileLog.js ADDED
@@ -0,0 +1,136 @@
1
+ import { closeSync, constants, openSync, writeSync } from "node:fs";
2
+ /**
3
+ * Persistent run log: every test's pass/failure followed by its captured
4
+ * output, appended to `.alchemy/log/test.log` as the run progresses —
5
+ * color-free so agents and tooling can read it directly.
6
+ */
7
+ import * as Effect from "effect/Effect";
8
+ import * as FileSystem from "effect/FileSystem";
9
+ import * as Option from "effect/Option";
10
+ import * as Path from "effect/Path";
11
+ import { formatPlanPreview } from "./Plan.js";
12
+ const formatDuration = (ms) => ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`;
13
+ const formatLogs = (logs) => logs.map((log) => log.message).join("\n");
14
+ const GLYPH = {
15
+ pass: "PASS",
16
+ fail: "FAIL",
17
+ skip: "SKIP",
18
+ todo: "TODO",
19
+ };
20
+ /** Render an event as a log chunk; `undefined` = nothing to write. */
21
+ export const formatEvent = (event) => {
22
+ switch (event._tag) {
23
+ case "PlanPreview":
24
+ return `${formatPlanPreview(event.phases)}\n`;
25
+ case "PlanPhaseStart":
26
+ return `Plan phase ${event.phase}/${event.phases}: ${event.tests} tests\n`;
27
+ case "RunStart":
28
+ return `running ${event.tests.length} tests from ${event.files} files (${new Date().toISOString()})\n\n`;
29
+ case "TestEnd": {
30
+ const title = `${event.test.file} > ${event.test.titlePath.join(" > ")}`;
31
+ const retries = event.result.retries > 0 ? ` [retried x${event.result.retries}]` : "";
32
+ const lines = [
33
+ `${GLYPH[event.result.status]} ${title} (${formatDuration(event.result.durationMs)})${retries}`,
34
+ ];
35
+ if (event.result.error !== undefined) {
36
+ lines.push(event.result.error);
37
+ }
38
+ if (event.result.logs.length > 0) {
39
+ lines.push("--- captured output ---");
40
+ lines.push(formatLogs(event.result.logs));
41
+ lines.push("--- end output ---");
42
+ }
43
+ // One line per plain result; a blank separator only after multi-line
44
+ // entries (error/output blocks) so they don't run into the next entry.
45
+ return lines.length === 1 ? `${lines[0]}\n` : `${lines.join("\n")}\n\n`;
46
+ }
47
+ case "FileEnd": {
48
+ const lines = [];
49
+ if (event.error !== undefined) {
50
+ lines.push(`FAIL ${event.file} failed to run`, event.error);
51
+ }
52
+ if (event.logs.length > 0) {
53
+ lines.push(`--- file hook output (${event.file}) ---`);
54
+ lines.push(formatLogs(event.logs));
55
+ lines.push("--- end output ---");
56
+ }
57
+ return lines.length === 0 ? undefined : `${lines.join("\n")}\n\n`;
58
+ }
59
+ case "RunEnd": {
60
+ const s = event.summary;
61
+ return (`Tests: ${s.failed} failed | ${s.passed} passed` +
62
+ (s.skipped > 0 ? ` | ${s.skipped} skipped` : "") +
63
+ (s.todo > 0 ? ` | ${s.todo} todo` : "") +
64
+ (s.plan
65
+ ? ` | ${s.plan.found} found | ${s.plan.selected} selected by plan | ${s.plan.excluded} excluded by plan`
66
+ : "") +
67
+ ` (${s.files} files, ${formatDuration(s.durationMs)})\n`);
68
+ }
69
+ default:
70
+ return undefined;
71
+ }
72
+ };
73
+ const LOG_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
74
+ /**
75
+ * Delete sibling run logs whose mtime (from stat — file names are never
76
+ * trusted) is older than a week, so the per-run log directory can't grow
77
+ * unboundedly. Best-effort: failures are ignored.
78
+ */
79
+ const pruneOldLogs = Effect.fn(function* (dir) {
80
+ const fs = yield* FileSystem.FileSystem;
81
+ const path = yield* Path.Path;
82
+ const entries = yield* fs
83
+ .readDirectory(dir)
84
+ .pipe(Effect.orElseSucceed(() => []));
85
+ const cutoff = Date.now() - LOG_RETENTION_MS;
86
+ for (const entry of entries) {
87
+ if (!entry.endsWith(".log"))
88
+ continue;
89
+ const file = path.join(dir, entry);
90
+ const info = yield* fs.stat(file).pipe(Effect.orElseSucceed(() => undefined));
91
+ const mtime = info === undefined ? undefined : Option.getOrUndefined(info.mtime);
92
+ if (mtime !== undefined && mtime.getTime() < cutoff) {
93
+ yield* fs.remove(file).pipe(Effect.ignore);
94
+ }
95
+ }
96
+ });
97
+ /** Create (truncate) the log file and return an event appender. */
98
+ export const makeFileLog = Effect.fn(function* (logFile) {
99
+ const fs = yield* FileSystem.FileSystem;
100
+ const path = yield* Path.Path;
101
+ yield* fs.makeDirectory(path.dirname(logFile), { recursive: true }).pipe(Effect.ignore);
102
+ yield* pruneOldLogs(path.dirname(logFile));
103
+ const fd = yield* Effect.sync(() => openSync(logFile, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_APPEND));
104
+ let closed = false;
105
+ const write = (text) => {
106
+ if (closed)
107
+ return;
108
+ const bytes = Buffer.from(text);
109
+ let offset = 0;
110
+ while (offset < bytes.length) {
111
+ offset += writeSync(fd, bytes, offset, bytes.length - offset);
112
+ }
113
+ };
114
+ const append = (event) => Effect.sync(() => {
115
+ const chunk = formatEvent(event);
116
+ if (chunk !== undefined)
117
+ write(chunk);
118
+ });
119
+ const appendLine = (label, entry) => {
120
+ write(`[${label}] ${entry.message}\n`);
121
+ };
122
+ const close = Effect.sync(() => {
123
+ if (closed)
124
+ return;
125
+ closed = true;
126
+ closeSync(fd);
127
+ });
128
+ yield* Effect.addFinalizer(() => close);
129
+ return {
130
+ append,
131
+ appendHookLine: (file, entry) => appendLine(`hook ${file}`, entry),
132
+ appendTestLine: (id, entry) => appendLine(`test ${id}`, entry),
133
+ close,
134
+ };
135
+ });
136
+ //# sourceMappingURL=FileLog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileLog.js","sourceRoot":"","sources":["../src/FileLog.ts"],"names":[],"mappings":"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"}
package/lib/Http.d.ts ADDED
@@ -0,0 +1,109 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as HttpClient from "effect/http/HttpClient";
3
+ import * as HttpClientRequest from "effect/http/HttpClientRequest";
4
+ import type { HttpClientResponse } from "effect/http/HttpClientResponse";
5
+ import * as Layer from "effect/Layer";
6
+ import * as RpcClient from "effect/rpc/RpcClient";
7
+ import * as RpcSerialization from "effect/rpc/RpcSerialization";
8
+ import * as Schedule from "effect/Schedule";
9
+ declare const WorkerNotReady_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
10
+ readonly _tag: "WorkerNotReady";
11
+ } & Readonly<A>;
12
+ /**
13
+ * A freshly-deployed Cloudflare Worker is not instantly reachable over HTTP.
14
+ * Its `workers.dev` route, the script, and each binding (R2 / D1 / DO / Secrets
15
+ * Store) propagate to the edge independently and asynchronously, so the first
16
+ * requests to a new URL can transiently return:
17
+ *
18
+ * - `404` while the `workers.dev` subdomain / route is still propagating
19
+ * (Cloudflare serves its "There is nothing here yet" placeholder), or
20
+ * - `5xx` while the script is up but a binding it depends on isn't ready yet.
21
+ *
22
+ * This is ordinary eventual consistency that belongs at the call site, not in
23
+ * the resource provider — the provider returning before every edge PoP has
24
+ * converged is correct. Consumers ride out the window by retrying the request.
25
+ */
26
+ export declare class WorkerNotReady extends WorkerNotReady_base<{
27
+ status: number;
28
+ }> {
29
+ }
30
+ export interface WhenReadyOptions {
31
+ /** Max retry attempts before surfacing {@link WorkerNotReady}. Default `20`. */
32
+ times?: number;
33
+ }
34
+ /**
35
+ * Execute an arbitrary {@link HttpClientRequest.HttpClientRequest}, retrying
36
+ * through the Cloudflare cold-start window ({@link isColdStartStatus}) until the
37
+ * Worker serves a non-transient response. The returned response can carry any
38
+ * non-cold-start status (e.g. `200`, `202`, `401`) for the caller to assert on.
39
+ */
40
+ export declare const executeWhenReady: (request: HttpClientRequest.HttpClientRequest, options?: WhenReadyOptions) => Effect.Effect<HttpClientResponse, unknown, HttpClient.HttpClient>;
41
+ /**
42
+ * Convenience wrapper over {@link executeWhenReady} for a plain `GET`.
43
+ */
44
+ export declare const getWhenReady: (url: string, options?: WhenReadyOptions) => Effect.Effect<HttpClientResponse, unknown, HttpClient.HttpClient>;
45
+ /**
46
+ * Options for the edge-transient response guard applied by
47
+ * {@link guardContentType} / {@link rpcClientLayer}. Pass per suite via
48
+ * `Test.make({ http: { ... } })` or per call site.
49
+ */
50
+ export interface EdgeGuardOptions {
51
+ /**
52
+ * Retry schedule for edge-transient responses (and transport errors).
53
+ * Default: exponential from 500ms capped at 3s.
54
+ */
55
+ schedule?: Schedule.Schedule<unknown, unknown>;
56
+ /** Max transport-level retry attempts. Default `5`. */
57
+ times?: number;
58
+ }
59
+ /**
60
+ * Guard an `HttpClient` against edge-generated bodies on a freshly deployed
61
+ * Worker URL.
62
+ *
63
+ * Protocol clients like effect RPC's `layerProtocolHttp` never inspect the
64
+ * response status or content-type — they pipe the raw body straight into the
65
+ * serialization parser, so every edge-generated HTML page (the workers.dev
66
+ * placeholder, which serves with HTTP **200**; 1101/1102 error pages;
67
+ * 429/1015 rate limits) surfaces as an opaque decode defect (e.g.
68
+ * `RpcClientDefect: Error decoding HTTP response`).
69
+ *
70
+ * This transform rejects any response whose `content-type` doesn't match the
71
+ * expected one as a typed `HttpClientError` that records the status + a body
72
+ * snippet — so a failure names its cause — and retries the request through a
73
+ * bounded schedule so callers ride out the cold-start window
74
+ * ({@link WorkerNotReady} documents why this belongs at the call site).
75
+ */
76
+ export declare const guardContentType: (contentType: string, options?: EdgeGuardOptions) => (client: HttpClient.HttpClient) => HttpClient.HttpClient;
77
+ /**
78
+ * A fetch-backed `HttpClient` layer wrapped with {@link guardContentType}.
79
+ *
80
+ * Deliberately a standalone layer rather than a transform of the ambient
81
+ * test `HttpClient`: the ambient client also serves the engine's own cloud
82
+ * API calls during `deploy`/`destroy` and distilled SDK calls in
83
+ * `test.provider` bodies, which must NOT be subjected to this guard.
84
+ */
85
+ export declare const guardedFetchLayer: (contentType: string, options?: EdgeGuardOptions) => Layer.Layer<HttpClient.HttpClient>;
86
+ /**
87
+ * Complete RPC-over-HTTP client transport for driving a deployed Worker:
88
+ * `RpcClient.layerProtocolHttp` + the given serialization (ndjson by
89
+ * default) + a {@link guardedFetchLayer} expecting that serialization's
90
+ * content-type.
91
+ *
92
+ * ```ts
93
+ * const client = yield* RpcClient.make(WorkerRpcs);
94
+ * // ...
95
+ * }).pipe(Effect.scoped, Effect.provide(Test.Http.rpcClientLayer(url)));
96
+ * ```
97
+ */
98
+ export declare const rpcClientLayer: (url: string, options?: EdgeGuardOptions & {
99
+ /** RPC wire serialization. Default {@link RpcSerialization.ndjson}. */
100
+ serialization?: RpcSerialization.RpcSerialization["Service"];
101
+ /**
102
+ * Set `false` to skip the edge guard entirely and use a plain fetch
103
+ * transport — e.g. for a test that asserts on the raw edge behavior
104
+ * itself. Default `true`.
105
+ */
106
+ guard?: boolean;
107
+ }) => Layer.Layer<RpcClient.Protocol>;
108
+ export {};
109
+ //# sourceMappingURL=Http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Http.d.ts","sourceRoot":"","sources":["../src/Http.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAErD,OAAO,KAAK,iBAAiB,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,gBAAgB,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;;;;AAE5C;;;;;;;;;;;;;GAaG;AACH,qBAAa,cAAe,SAAQ,oBAAmC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;CAAG;AAU7F,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,YAClB,iBAAiB,CAAC,iBAAiB,YAClC,gBAAgB,KACzB,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE,OAAO,EAAE,UAAU,CAAC,UAAU,CAiB/D,CAAC;AAEL;;GAEG;AACH,eAAO,MAAM,YAAY,QAClB,MAAM,YACD,gBAAgB,KACzB,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE,OAAO,EAAE,UAAU,CAAC,UAAU,CACZ,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC/C,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,gBACb,MAAM,YAAY,gBAAgB,cACvC,UAAU,CAAC,UAAU,KAAG,UAAU,CAAC,UA4BzC,CAAC;AAEN;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,gBACf,MAAM,YACT,gBAAgB,KACzB,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,CAMU,CAAC;AAE/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,QACpB,MAAM,YACD,gBAAgB,GAAG;IAC3B,uEAAuE;IACvE,aAAa,CAAC,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC7D;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,KACA,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAUhC,CAAC"}
package/lib/Http.js ADDED
@@ -0,0 +1,123 @@
1
+ import * as Data from "effect/Data";
2
+ import * as Effect from "effect/Effect";
3
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
4
+ import * as HttpClient from "effect/http/HttpClient";
5
+ import { DecodeError, HttpClientError } from "effect/http/HttpClientError";
6
+ import * as HttpClientRequest from "effect/http/HttpClientRequest";
7
+ import * as Layer from "effect/Layer";
8
+ import * as RpcClient from "effect/rpc/RpcClient";
9
+ import * as RpcSerialization from "effect/rpc/RpcSerialization";
10
+ import * as Schedule from "effect/Schedule";
11
+ /**
12
+ * A freshly-deployed Cloudflare Worker is not instantly reachable over HTTP.
13
+ * Its `workers.dev` route, the script, and each binding (R2 / D1 / DO / Secrets
14
+ * Store) propagate to the edge independently and asynchronously, so the first
15
+ * requests to a new URL can transiently return:
16
+ *
17
+ * - `404` while the `workers.dev` subdomain / route is still propagating
18
+ * (Cloudflare serves its "There is nothing here yet" placeholder), or
19
+ * - `5xx` while the script is up but a binding it depends on isn't ready yet.
20
+ *
21
+ * This is ordinary eventual consistency that belongs at the call site, not in
22
+ * the resource provider — the provider returning before every edge PoP has
23
+ * converged is correct. Consumers ride out the window by retrying the request.
24
+ */
25
+ export class WorkerNotReady extends Data.TaggedError("WorkerNotReady") {
26
+ }
27
+ /**
28
+ * Status codes that indicate the edge hasn't finished converging on a fresh
29
+ * deploy. Deliberate client errors (`401`, `403`, `400`, …) are NOT in this
30
+ * set, so assertions on those statuses still observe them immediately rather
31
+ * than being retried away.
32
+ */
33
+ const isColdStartStatus = (status) => status === 404 || status >= 500;
34
+ /**
35
+ * Execute an arbitrary {@link HttpClientRequest.HttpClientRequest}, retrying
36
+ * through the Cloudflare cold-start window ({@link isColdStartStatus}) until the
37
+ * Worker serves a non-transient response. The returned response can carry any
38
+ * non-cold-start status (e.g. `200`, `202`, `401`) for the caller to assert on.
39
+ */
40
+ export const executeWhenReady = (request, options) => Effect.gen(function* () {
41
+ const client = yield* HttpClient.HttpClient;
42
+ return yield* client.execute(request).pipe(Effect.flatMap((response) => isColdStartStatus(response.status)
43
+ ? Effect.fail(new WorkerNotReady({ status: response.status }))
44
+ : Effect.succeed(response)), Effect.retry({
45
+ while: (error) => error instanceof WorkerNotReady,
46
+ schedule: Schedule.max([
47
+ Schedule.exponential("500 millis"),
48
+ Schedule.recurs(options?.times ?? 20),
49
+ ]),
50
+ }));
51
+ });
52
+ /**
53
+ * Convenience wrapper over {@link executeWhenReady} for a plain `GET`.
54
+ */
55
+ export const getWhenReady = (url, options) => executeWhenReady(HttpClientRequest.get(url), options);
56
+ const defaultGuardSchedule = Schedule.min([
57
+ Schedule.exponential("500 millis"),
58
+ Schedule.spaced("3 seconds"),
59
+ ]);
60
+ /**
61
+ * Guard an `HttpClient` against edge-generated bodies on a freshly deployed
62
+ * Worker URL.
63
+ *
64
+ * Protocol clients like effect RPC's `layerProtocolHttp` never inspect the
65
+ * response status or content-type — they pipe the raw body straight into the
66
+ * serialization parser, so every edge-generated HTML page (the workers.dev
67
+ * placeholder, which serves with HTTP **200**; 1101/1102 error pages;
68
+ * 429/1015 rate limits) surfaces as an opaque decode defect (e.g.
69
+ * `RpcClientDefect: Error decoding HTTP response`).
70
+ *
71
+ * This transform rejects any response whose `content-type` doesn't match the
72
+ * expected one as a typed `HttpClientError` that records the status + a body
73
+ * snippet — so a failure names its cause — and retries the request through a
74
+ * bounded schedule so callers ride out the cold-start window
75
+ * ({@link WorkerNotReady} documents why this belongs at the call site).
76
+ */
77
+ export const guardContentType = (contentType, options) => (client) => client.pipe(HttpClient.transform((effect, request) => Effect.flatMap(effect, (response) => {
78
+ const observed = response.headers["content-type"] ?? "";
79
+ if (observed.includes(contentType)) {
80
+ return Effect.succeed(response);
81
+ }
82
+ return response.text.pipe(Effect.orElseSucceed(() => "<unreadable body>"), Effect.flatMap((body) => Effect.fail(new HttpClientError({
83
+ reason: new DecodeError({
84
+ request,
85
+ response,
86
+ description: `expected "${contentType}", got "${observed}" (status ${response.status}): ${body.slice(0, 200)}`,
87
+ }),
88
+ }))));
89
+ })), HttpClient.retry({
90
+ schedule: options?.schedule ?? defaultGuardSchedule,
91
+ times: options?.times ?? 5,
92
+ }));
93
+ /**
94
+ * A fetch-backed `HttpClient` layer wrapped with {@link guardContentType}.
95
+ *
96
+ * Deliberately a standalone layer rather than a transform of the ambient
97
+ * test `HttpClient`: the ambient client also serves the engine's own cloud
98
+ * API calls during `deploy`/`destroy` and distilled SDK calls in
99
+ * `test.provider` bodies, which must NOT be subjected to this guard.
100
+ */
101
+ export const guardedFetchLayer = (contentType, options) => Layer.effect(HttpClient.HttpClient)(Effect.gen(function* () {
102
+ const client = yield* HttpClient.HttpClient;
103
+ return guardContentType(contentType, options)(client);
104
+ })).pipe(Layer.provide(FetchHttpClient.layer));
105
+ /**
106
+ * Complete RPC-over-HTTP client transport for driving a deployed Worker:
107
+ * `RpcClient.layerProtocolHttp` + the given serialization (ndjson by
108
+ * default) + a {@link guardedFetchLayer} expecting that serialization's
109
+ * content-type.
110
+ *
111
+ * ```ts
112
+ * const client = yield* RpcClient.make(WorkerRpcs);
113
+ * // ...
114
+ * }).pipe(Effect.scoped, Effect.provide(Test.Http.rpcClientLayer(url)));
115
+ * ```
116
+ */
117
+ export const rpcClientLayer = (url, options) => {
118
+ const serialization = options?.serialization ?? RpcSerialization.ndjson;
119
+ return RpcClient.layerProtocolHttp({ url }).pipe(Layer.provide(options?.guard === false
120
+ ? FetchHttpClient.layer
121
+ : guardedFetchLayer(serialization.contentType, options)), Layer.provide(Layer.succeed(RpcSerialization.RpcSerialization, serialization)));
122
+ };
123
+ //# sourceMappingURL=Http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Http.js","sourceRoot":"","sources":["../src/Http.ts"],"names":[],"mappings":"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"}
package/lib/Model.d.ts ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Core data model for the alchemy-test harness.
3
+ *
4
+ * A test file is collected into a tree of {@link Suite}s and {@link TestCase}s
5
+ * by evaluating its module body (describe/test calls register nodes). The
6
+ * runner then walks the tree and executes every test as an Effect.
7
+ */
8
+ import type * as Effect from "effect/Effect";
9
+ import { type Tags } from "./Tags.ts";
10
+ /** Execution mode attached to a suite or test at registration time. */
11
+ export type Mode = "run" | "skip" | "only" | "todo";
12
+ /** A single log line captured from a test's Effect Logger or Console. */
13
+ export interface LogEntry {
14
+ readonly level: string;
15
+ readonly message: string;
16
+ readonly time: Date;
17
+ }
18
+ /**
19
+ * The body of a test. Always an Effect — plain-function tests are wrapped
20
+ * at registration time. The Effect must be self-contained (R = never);
21
+ * wrapping (scope, TestClock/TestConsole, shared layers) happens in the
22
+ * harness closures, NOT in the runner.
23
+ */
24
+ export type TestBody = () => Effect.Effect<unknown, unknown, never>;
25
+ /** A hook body. Same contract as {@link TestBody}. */
26
+ export type HookBody = () => Effect.Effect<unknown, unknown, never>;
27
+ /** `true` for the whole-process lock, or a label naming a narrower lock. */
28
+ export type Exclusive = boolean | string;
29
+ export interface Hook {
30
+ readonly body: HookBody;
31
+ readonly timeout?: number | undefined;
32
+ /**
33
+ * `true` takes the whole-process write lock for this hook. A label (e.g.
34
+ * `"hetzner"`) only serializes the hook with tests and hooks holding the
35
+ * same label — use it for deploys that consume a scarce cloud quota.
36
+ */
37
+ readonly exclusive?: Exclusive;
38
+ }
39
+ export interface TestCase {
40
+ /** Deduplicated labels, including tags inherited from suites. */
41
+ readonly tags: ReadonlyArray<string>;
42
+ /** Inherited tags requiring explicit selection. */
43
+ readonly optInTags: ReadonlyArray<string>;
44
+ readonly type: "test";
45
+ readonly name: string;
46
+ readonly mode: Mode;
47
+ /** Invert the result: the test passes only if the body fails. */
48
+ readonly fails?: boolean;
49
+ /**
50
+ * `true` runs this test with the whole-process write lock: no other test
51
+ * in the run executes concurrently with it. Required for tests that mutate
52
+ * process-global state (e.g. `process.env.PATH`) — the runner executes
53
+ * everything in ONE bun process, so such mutations are visible everywhere.
54
+ *
55
+ * A label (e.g. `"hetzner"`) only serializes the test with others holding
56
+ * the same label — use it for a shared cloud quota, not global state.
57
+ */
58
+ readonly exclusive?: Exclusive;
59
+ /** Override the run-wide retry budget for this test. */
60
+ readonly retry?: number | undefined;
61
+ readonly timeout?: number | undefined;
62
+ readonly body: TestBody | undefined;
63
+ readonly parent: Suite;
64
+ }
65
+ export interface Suite {
66
+ /** Deduplicated labels, including tags inherited from parent suites. */
67
+ readonly tags: ReadonlyArray<string>;
68
+ /** Inherited tags requiring explicit selection. */
69
+ readonly optInTags: ReadonlyArray<string>;
70
+ readonly type: "suite";
71
+ readonly name: string;
72
+ mode: Mode;
73
+ /** When true, children run one at a time (the default; describe.concurrent opts out). */
74
+ sequential: boolean;
75
+ readonly children: Array<Suite | TestCase>;
76
+ readonly beforeAll: Array<Hook>;
77
+ readonly afterAll: Array<Hook>;
78
+ readonly beforeEach: Array<Hook>;
79
+ readonly afterEach: Array<Hook>;
80
+ readonly parent: Suite | undefined;
81
+ }
82
+ /** The root suite of a single test file. */
83
+ export interface FileSuite extends Suite {
84
+ /** Path relative to the run root, e.g. `test/Cloudflare/R2/Bucket.test.ts`. */
85
+ readonly file: string;
86
+ /** Framework resources, disposed even when user hooks are skipped. */
87
+ readonly cleanups: Array<Hook>;
88
+ }
89
+ export declare const makeSuite: (name: string, parent: Suite | undefined, mode?: Mode, tags?: Tags, optInTags?: Tags) => Suite;
90
+ export declare const makeFileSuite: (file: string) => FileSuite;
91
+ /** Full title path from the file root down to (and including) this node. */
92
+ export declare const titlePath: (node: Suite | TestCase) => ReadonlyArray<string>;
93
+ export declare const fullTitle: (node: Suite | TestCase) => string;
94
+ /** Walk every test in a suite subtree (depth-first, registration order). */
95
+ export declare const forEachTest: (suite: Suite, f: (test: TestCase) => void) => void;
96
+ /** True if the subtree contains a node marked `only`. */
97
+ export declare const containsOnly: (suite: Suite) => boolean;
98
+ //# sourceMappingURL=Model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../src/Model.ts"],"names":[],"mappings":"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"}
package/lib/Model.js ADDED
@@ -0,0 +1,59 @@
1
+ import { mergeTags } from "./Tags.js";
2
+ export const makeSuite = (name, parent, mode = "run", tags, optInTags) => ({
3
+ tags: mergeTags(parent?.tags ?? [], tags),
4
+ optInTags: mergeTags(parent?.optInTags ?? [], optInTags),
5
+ type: "suite",
6
+ name,
7
+ mode,
8
+ sequential: true,
9
+ children: [],
10
+ beforeAll: [],
11
+ afterAll: [],
12
+ beforeEach: [],
13
+ afterEach: [],
14
+ parent,
15
+ });
16
+ export const makeFileSuite = (file) => ({
17
+ ...makeSuite(file, undefined),
18
+ file,
19
+ cleanups: [],
20
+ });
21
+ /** Full title path from the file root down to (and including) this node. */
22
+ export const titlePath = (node) => {
23
+ const parts = [];
24
+ let current = node;
25
+ while (current !== undefined) {
26
+ // Skip the file-root suite name (it's reported separately as `file`).
27
+ if (current.parent !== undefined || current.type === "test") {
28
+ parts.unshift(current.name);
29
+ }
30
+ else if (current.parent === undefined && current.type === "suite") {
31
+ // file root — stop
32
+ }
33
+ current = current.parent;
34
+ }
35
+ return parts;
36
+ };
37
+ export const fullTitle = (node) => titlePath(node).join(" > ");
38
+ /** Walk every test in a suite subtree (depth-first, registration order). */
39
+ export const forEachTest = (suite, f) => {
40
+ for (const child of suite.children) {
41
+ if (child.type === "test")
42
+ f(child);
43
+ else
44
+ forEachTest(child, f);
45
+ }
46
+ };
47
+ /** True if the subtree contains a node marked `only`. */
48
+ export const containsOnly = (suite) => {
49
+ if (suite.mode === "only")
50
+ return true;
51
+ for (const child of suite.children) {
52
+ if (child.type === "test" && child.mode === "only")
53
+ return true;
54
+ if (child.type === "suite" && containsOnly(child))
55
+ return true;
56
+ }
57
+ return false;
58
+ };
59
+ //# sourceMappingURL=Model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Model.js","sourceRoot":"","sources":["../src/Model.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,SAAS,EAAa,MAAM,WAAW,CAAC;AA0FjD,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,IAAY,EACZ,MAAyB,EACzB,IAAI,GAAS,KAAK,EAClB,IAAW,EACX,SAAgB,EACT,EAAE,CAAC,CAAC;IACX,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC;IACzC,SAAS,EAAE,SAAS,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,EAAE,SAAS,CAAC;IACxD,IAAI,EAAE,OAAO;IACb,IAAI;IACJ,IAAI;IACJ,UAAU,EAAE,IAAI;IAChB,QAAQ,EAAE,EAAE;IACZ,SAAS,EAAE,EAAE;IACb,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,SAAS,EAAE,EAAE;IACb,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAY,EAAa,EAAE,CAAC,CAAC;IACzD,GAAG,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC;IAC7B,IAAI;IACJ,QAAQ,EAAE,EAAE;CACb,CAAC,CAAC;AAEH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAsB,EAAyB,EAAE;IACzE,MAAM,KAAK,GAAkB,EAAE,CAAC;IAChC,IAAI,OAAO,GAAiC,IAAI,CAAC;IACjD,OAAO,OAAO,KAAK,SAAS,EAAE,CAAC;QAC7B,sEAAsE;QACtE,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC5D,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;aAAM,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACpE,mBAAmB;QACrB,CAAC;QACD,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAC3B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAsB,EAAU,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAEzF,4EAA4E;AAC5E,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAY,EAAE,CAA2B,EAAQ,EAAE;IAC7E,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACnC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;YAAE,CAAC,CAAC,KAAK,CAAC,CAAC;;YAC/B,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC,CAAC;AAEF,yDAAyD;AACzD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAY,EAAW,EAAE;IACpD,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACvC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACnC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC;QAChE,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,YAAY,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IACjE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Line-oriented reporter for non-interactive terminals and CI.
3
+ *
4
+ * Reports progress through the collection phase (importing a large suite's
5
+ * files takes tens of seconds before any test can run), then logs one line per
6
+ * test as it finishes, prefixed with a `[done/total]` counter (failures include
7
+ * their error and captured output inline); if nothing finishes for 10s it
8
+ * prints the list of currently-running tests; at the end every failure is
9
+ * repeated consolidated.
10
+ */
11
+ import * as Effect from "effect/Effect";
12
+ import * as Layer from "effect/Layer";
13
+ import type { LogEntry } from "./Model.ts";
14
+ import { Reporter, type RunSummary } from "./Reporter.ts";
15
+ export declare const printSummary: (summary: RunSummary,
16
+ /** Buffered deploy/destroy output per file, printed once per failing file. */
17
+ hookLogs?: ReadonlyMap<string, ReadonlyArray<LogEntry>>) => Effect.Effect<void>;
18
+ export declare const PlainReporterLive: Layer.Layer<Reporter>;
19
+ //# sourceMappingURL=PlainReporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlainReporter.d.ts","sourceRoot":"","sources":["../src/PlainReporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,OAAO,EAAE,QAAQ,EAAE,KAAK,UAAU,EAAmC,MAAM,eAAe,CAAC;AA4S3F,eAAO,MAAM,YAAY,YACd,UAAU;AACnB,8EAA8E;WACnE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC,KACtD,MAAM,CAAC,MAAM,CAAC,IAAI,CA4DjB,CAAC;AAEL,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ,CA6ClD,CAAC"}