@livestore/utils-dev 0.0.0-snapshot-97a63e41c38787f5564c4278a39840dcabe0dc05 → 0.0.0-snapshot-0dd60208a53471f56fc4397eac81fac2bd5062fe

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 (82) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/node/DockerComposeService/DockerComposeService.d.ts +58 -0
  3. package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +1 -0
  4. package/dist/node/DockerComposeService/DockerComposeService.js +144 -0
  5. package/dist/node/DockerComposeService/DockerComposeService.js.map +1 -0
  6. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +2 -0
  7. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +1 -0
  8. package/dist/node/DockerComposeService/DockerComposeService.test.js +64 -0
  9. package/dist/node/DockerComposeService/DockerComposeService.test.js.map +1 -0
  10. package/dist/node/FileLogger.d.ts +14 -0
  11. package/dist/node/FileLogger.d.ts.map +1 -0
  12. package/dist/node/FileLogger.js +151 -0
  13. package/dist/node/FileLogger.js.map +1 -0
  14. package/dist/node/cmd-log.d.ts +21 -0
  15. package/dist/node/cmd-log.d.ts.map +1 -0
  16. package/dist/node/cmd-log.js +50 -0
  17. package/dist/node/cmd-log.js.map +1 -0
  18. package/dist/node/cmd.d.ts +36 -0
  19. package/dist/node/cmd.d.ts.map +1 -0
  20. package/dist/node/cmd.js +234 -0
  21. package/dist/node/cmd.js.map +1 -0
  22. package/dist/node/cmd.test.d.ts +2 -0
  23. package/dist/node/cmd.test.d.ts.map +1 -0
  24. package/dist/node/cmd.test.js +101 -0
  25. package/dist/node/cmd.test.js.map +1 -0
  26. package/dist/node/mod.d.ts +5 -13
  27. package/dist/node/mod.d.ts.map +1 -1
  28. package/dist/node/mod.js +67 -42
  29. package/dist/node/mod.js.map +1 -1
  30. package/dist/node-vitest/Vitest.d.ts +52 -0
  31. package/dist/node-vitest/Vitest.d.ts.map +1 -0
  32. package/dist/node-vitest/Vitest.js +98 -0
  33. package/dist/node-vitest/Vitest.js.map +1 -0
  34. package/dist/node-vitest/Vitest.test.d.ts +2 -0
  35. package/dist/node-vitest/Vitest.test.d.ts.map +1 -0
  36. package/dist/node-vitest/Vitest.test.js +70 -0
  37. package/dist/node-vitest/Vitest.test.js.map +1 -0
  38. package/dist/node-vitest/global.d.ts +2 -0
  39. package/dist/node-vitest/global.d.ts.map +1 -0
  40. package/dist/node-vitest/{polyfill.js → global.js} +1 -1
  41. package/dist/node-vitest/global.js.map +1 -0
  42. package/dist/node-vitest/mod.d.ts +2 -1
  43. package/dist/node-vitest/mod.d.ts.map +1 -1
  44. package/dist/node-vitest/mod.js +2 -1
  45. package/dist/node-vitest/mod.js.map +1 -1
  46. package/dist/wrangler/WranglerDevServer.d.ts +52 -0
  47. package/dist/wrangler/WranglerDevServer.d.ts.map +1 -0
  48. package/dist/wrangler/WranglerDevServer.js +90 -0
  49. package/dist/wrangler/WranglerDevServer.js.map +1 -0
  50. package/dist/wrangler/WranglerDevServer.test.d.ts +2 -0
  51. package/dist/wrangler/WranglerDevServer.test.d.ts.map +1 -0
  52. package/dist/wrangler/WranglerDevServer.test.js +77 -0
  53. package/dist/wrangler/WranglerDevServer.test.js.map +1 -0
  54. package/dist/wrangler/fixtures/cf-worker.d.ts +8 -0
  55. package/dist/wrangler/fixtures/cf-worker.d.ts.map +1 -0
  56. package/dist/wrangler/fixtures/cf-worker.js +11 -0
  57. package/dist/wrangler/fixtures/cf-worker.js.map +1 -0
  58. package/dist/wrangler/mod.d.ts +2 -0
  59. package/dist/wrangler/mod.d.ts.map +1 -0
  60. package/dist/wrangler/mod.js +2 -0
  61. package/dist/wrangler/mod.js.map +1 -0
  62. package/package.json +16 -22
  63. package/src/node/DockerComposeService/DockerComposeService.test.ts +91 -0
  64. package/src/node/DockerComposeService/DockerComposeService.ts +328 -0
  65. package/src/node/DockerComposeService/test-fixtures/docker-compose.yml +4 -0
  66. package/src/node/FileLogger.ts +206 -0
  67. package/src/node/cmd-log.ts +92 -0
  68. package/src/node/cmd.test.ts +129 -0
  69. package/src/node/cmd.ts +419 -0
  70. package/src/node/mod.ts +81 -82
  71. package/src/node-vitest/Vitest.test.ts +112 -0
  72. package/src/node-vitest/Vitest.ts +238 -0
  73. package/src/node-vitest/mod.ts +3 -1
  74. package/src/wrangler/WranglerDevServer.test.ts +133 -0
  75. package/src/wrangler/WranglerDevServer.ts +180 -0
  76. package/src/wrangler/fixtures/cf-worker.ts +11 -0
  77. package/src/wrangler/fixtures/wrangler.toml +11 -0
  78. package/src/wrangler/mod.ts +6 -0
  79. package/dist/node-vitest/polyfill.d.ts +0 -2
  80. package/dist/node-vitest/polyfill.d.ts.map +0 -1
  81. package/dist/node-vitest/polyfill.js.map +0 -1
  82. /package/src/node-vitest/{polyfill.ts → global.ts} +0 -0
package/src/node/mod.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { performance } from 'node:perf_hooks'
2
2
 
3
3
  import * as OtelNodeSdk from '@effect/opentelemetry/NodeSdk'
4
- import { IS_BUN, isNotUndefined, shouldNeverHappen } from '@livestore/utils'
5
- import type { CommandExecutor, PlatformError, Tracer } from '@livestore/utils/effect'
6
- import { Command, Config, Effect, identity, Layer, OtelTracer } from '@livestore/utils/effect'
4
+ import { IS_BUN, isNonEmptyString } from '@livestore/utils'
5
+ import type { Tracer } from '@livestore/utils/effect'
6
+ import { Config, Effect, FiberRef, Layer, LogLevel, OtelTracer } from '@livestore/utils/effect'
7
7
  import { OtelLiveDummy } from '@livestore/utils/node'
8
8
  import * as otel from '@opentelemetry/api'
9
9
  import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
@@ -11,8 +11,19 @@ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
11
11
  import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
12
12
  import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
13
13
 
14
- export { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
15
14
  export { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
15
+ export { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
16
+ export * from './cmd.ts'
17
+ export {
18
+ type DockerComposeArgs,
19
+ DockerComposeError,
20
+ type DockerComposeOperations,
21
+ DockerComposeService,
22
+ type LogsOptions,
23
+ type StartOptions,
24
+ startDockerComposeServicesScoped,
25
+ } from './DockerComposeService/DockerComposeService.ts'
26
+ export * as FileLogger from './FileLogger.ts'
16
27
 
17
28
  export const OtelLiveHttp = ({
18
29
  serviceName,
@@ -31,7 +42,9 @@ export const OtelLiveHttp = ({
31
42
  } = {}): Layer.Layer<OtelTracer.OtelTracer | Tracer.ParentSpan, never, never> =>
32
43
  Effect.gen(function* () {
33
44
  const configRes = yield* Config.all({
34
- exporterUrl: Config.string('OTEL_EXPORTER_OTLP_ENDPOINT'),
45
+ exporterUrl: Config.string('OTEL_EXPORTER_OTLP_ENDPOINT').pipe(
46
+ Config.validate({ message: 'OTEL_EXPORTER_OTLP_ENDPOINT must be set', validation: isNonEmptyString }),
47
+ ),
35
48
  serviceName: serviceName
36
49
  ? Config.succeed(serviceName)
37
50
  : Config.string('OTEL_SERVICE_NAME').pipe(Config.withDefault('livestore-utils-dev')),
@@ -42,7 +55,7 @@ export const OtelLiveHttp = ({
42
55
 
43
56
  if (configRes._tag === 'None') {
44
57
  const RootSpanLive = Layer.span('DummyRoot', {})
45
- return RootSpanLive.pipe(Layer.provide(OtelLiveDummy))
58
+ return RootSpanLive.pipe(Layer.provideMerge(OtelLiveDummy)) as any
46
59
  }
47
60
 
48
61
  const config = configRes.value
@@ -61,7 +74,19 @@ export const OtelLiveHttp = ({
61
74
  new OTLPTraceExporter({ url: `${config.exporterUrl}/v1/traces`, headers: {} }),
62
75
  { scheduledDelayMillis: 50 },
63
76
  ),
64
- }))
77
+ })).pipe(
78
+ // If an OpenTelemetry backend is not available, the `OtelNodeSdk` layer
79
+ // will ignore the error when attempting to connect and emit a debug log
80
+ // stating the reason for the error (in this case `ECONNREFUSED`). This
81
+ // can cause problems for programs which rely on clean `stdout` (e.g.
82
+ // command-line applications). To remedy this, the below code sets the
83
+ // minimum log level `FiberRef` to `"None"` for the duration of the
84
+ // `OtelNodeSdk`'s layer constructor.
85
+ //
86
+ // This can likely be removed when Livestore is migrated to the Effect
87
+ // native Otlp exporters.
88
+ Layer.locally(FiberRef.currentMinimumLogLevel, LogLevel.None),
89
+ )
65
90
 
66
91
  const RootSpanLive = Layer.span(config.rootSpanName, {
67
92
  attributes: { config, ...rootSpanAttributes },
@@ -79,10 +104,7 @@ export const OtelLiveHttp = ({
79
104
  const tracer = yield* OtelTracer.OtelTracer
80
105
  const currentSpan = yield* OtelTracer.currentOtelSpan
81
106
 
82
- const nodeTiming = performance.nodeTiming
83
-
84
- // TODO get rid of this workaround for Bun once Bun properly supports performance.nodeTiming
85
- const startTime = IS_BUN ? nodeTiming.startTime : performance.timeOrigin + nodeTiming.nodeStart
107
+ const { nodeTiming, endAbs, durationAttr } = computeBootstrapTiming()
86
108
 
87
109
  const bootSpan = tracer.startSpan(
88
110
  'node-bootstrap',
@@ -93,13 +115,13 @@ export const OtelLiveHttp = ({
93
115
  'node.timing.environment': nodeTiming.environment,
94
116
  'node.timing.bootstrapComplete': nodeTiming.bootstrapComplete,
95
117
  'node.timing.loopStart': nodeTiming.loopStart,
96
- 'node.timing.duration': nodeTiming.duration,
118
+ 'node.timing.duration': durationAttr,
97
119
  },
98
120
  },
99
121
  otel.trace.setSpanContext(otel.context.active(), currentSpan.spanContext()),
100
122
  )
101
123
 
102
- bootSpan.end(startTime + nodeTiming.duration)
124
+ bootSpan.end(endAbs)
103
125
  }).pipe(Effect.provide(layer), Effect.orDie)
104
126
  }
105
127
 
@@ -144,72 +166,49 @@ export const getTracingBackendUrl = (span: otel.Span) =>
144
166
  return `${grafanaEndpoint}/explore?${searchParams.toString()}`
145
167
  })
146
168
 
147
- export const cmd: (
148
- commandInput: string | (string | undefined)[],
149
- options?:
150
- | {
151
- cwd?: string
152
- shell?: boolean
153
- env?: Record<string, string | undefined>
154
- }
155
- | undefined,
156
- ) => Effect.Effect<CommandExecutor.ExitCode, PlatformError.PlatformError, CommandExecutor.CommandExecutor> = Effect.fn(
157
- 'cmd',
158
- )(function* (commandInput, options) {
159
- const cwd = options?.cwd ?? process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set')
160
- const [command, ...args] = Array.isArray(commandInput) ? commandInput.filter(isNotUndefined) : commandInput.split(' ')
161
-
162
- const debugEnvStr = Object.entries(options?.env ?? {})
163
- .map(([key, value]) => `${key}=${value} `)
164
- .join('')
165
- const commandDebugStr = debugEnvStr + [command, ...args].join(' ')
166
-
167
- yield* Effect.logDebug(`Running '${commandDebugStr}' in '${cwd}'`)
168
- yield* Effect.annotateCurrentSpan({ 'span.label': commandDebugStr, cwd, command, args })
169
-
170
- return yield* Command.make(command!, ...args).pipe(
171
- // TODO don't forward abort signal to the command
172
- Command.stdin('inherit'), // Forward stdin to the command
173
- Command.stdout('inherit'), // Stream stdout to process.stdout
174
- Command.stderr('inherit'), // Stream stderr to process.stderr
175
- Command.workingDirectory(cwd),
176
- options?.shell ? Command.runInShell(true) : identity,
177
- Command.env(options?.env ?? {}),
178
- Command.exitCode,
179
- Effect.tap((exitCode) => (exitCode === 0 ? Effect.void : Effect.die(`${commandDebugStr} failed`))),
180
- )
181
- })
182
-
183
- export const cmdText: (
184
- commandInput: string | (string | undefined)[],
185
- options?: {
186
- cwd?: string
187
- stderr?: 'inherit' | 'pipe'
188
- runInShell?: boolean
189
- env?: Record<string, string | undefined>
190
- },
191
- ) => Effect.Effect<string, PlatformError.PlatformError, CommandExecutor.CommandExecutor> = Effect.fn('cmdText')(
192
- function* (commandInput, options) {
193
- const cwd = options?.cwd ?? process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set')
194
- const [command, ...args] = Array.isArray(commandInput)
195
- ? commandInput.filter(isNotUndefined)
196
- : commandInput.split(' ')
197
- const debugEnvStr = Object.entries(options?.env ?? {})
198
- .map(([key, value]) => `${key}=${value} `)
199
- .join('')
200
-
201
- const commandDebugStr = debugEnvStr + [command, ...args].join(' ')
202
-
203
- yield* Effect.logDebug(`Running '${commandDebugStr}' in '${cwd}'`)
204
- yield* Effect.annotateCurrentSpan({ 'span.label': commandDebugStr, command, cwd })
205
-
206
- return yield* Command.make(command!, ...args).pipe(
207
- // inherit = Stream stderr to process.stderr, pipe = Stream stderr to process.stdout
208
- Command.stderr(options?.stderr ?? 'inherit'),
209
- Command.workingDirectory(cwd),
210
- options?.runInShell ? Command.runInShell(true) : identity,
211
- Command.env(options?.env ?? {}),
212
- Command.string,
213
- )
214
- },
215
- )
169
+ /**
170
+ * Compute absolute start/end timestamps for the Node.js bootstrap span in a
171
+ * way that works in both Node and Bun.
172
+ *
173
+ * Context: Bun's perf_hooks PerformanceNodeTiming currently throws when
174
+ * accessing standard PerformanceEntry getters like `startTime` and
175
+ * `duration`, and some fields differ in semantics (e.g. `nodeStart` appears
176
+ * as an epoch timestamp rather than an offset). See:
177
+ * https://github.com/oven-sh/bun/issues/23041
178
+ *
179
+ * We therefore avoid the problematic getters and derive absolute timestamps
180
+ * using fields that exist in both runtimes.
181
+ *
182
+ * TODO: Simplify to a single, non-branching computation once the Bun issue
183
+ * above is fixed and Bun matches Node's semantics for PerformanceNodeTiming.
184
+ */
185
+ const computeBootstrapTiming = () => {
186
+ const nodeTiming = performance.nodeTiming
187
+
188
+ // Absolute start time in ms since epoch.
189
+ const startAbs = IS_BUN
190
+ ? typeof nodeTiming.nodeStart === 'number'
191
+ ? nodeTiming.nodeStart
192
+ : performance.timeOrigin
193
+ : performance.timeOrigin + nodeTiming.nodeStart
194
+
195
+ // Absolute end time.
196
+ const endAbs = IS_BUN
197
+ ? (() => {
198
+ const { loopStart, bootstrapComplete } = nodeTiming
199
+ if (typeof loopStart === 'number' && loopStart > 0) return startAbs + loopStart
200
+ if (typeof bootstrapComplete === 'number' && bootstrapComplete >= startAbs) return bootstrapComplete
201
+ return startAbs + 1
202
+ })()
203
+ : startAbs + nodeTiming.duration
204
+
205
+ // Duration attribute value for the span.
206
+ const durationAttr = IS_BUN
207
+ ? (() => {
208
+ const { loopStart } = nodeTiming
209
+ return typeof loopStart === 'number' && loopStart > 0 ? loopStart : 0
210
+ })()
211
+ : nodeTiming.duration
212
+
213
+ return { nodeTiming, startAbs, endAbs, durationAttr } as const
214
+ }
@@ -0,0 +1,112 @@
1
+ import { Effect, FastCheck } from '@livestore/utils/effect'
2
+ import * as Vitest from './Vitest.ts'
3
+
4
+ // Demonstrate enhanced asProp functionality with clear shrinking progress
5
+ // This showcases the fix for the "Run 26/6" bug and accurate shrinking detection
6
+
7
+ Vitest.describe('Vitest.asProp', () => {
8
+ const IntArbitrary = FastCheck.integer({ min: 1, max: 100 })
9
+
10
+ // Always-passing test - should only show initial phase
11
+ Vitest.asProp(
12
+ Vitest.scopedLive,
13
+ 'always-pass test (shows only initial runs)',
14
+ [IntArbitrary],
15
+ (properties, _ctx, enhanced) =>
16
+ Effect.gen(function* () {
17
+ const [value] = properties
18
+ if (value === undefined) {
19
+ return yield* Effect.fail(new Error('Value is undefined'))
20
+ }
21
+
22
+ console.log(
23
+ `✅ ALWAYS-PASS [${enhanced._tag.toUpperCase()}]: ` +
24
+ (enhanced._tag === 'initial'
25
+ ? `Run ${enhanced.runIndex + 1}/${enhanced.numRuns}`
26
+ : `Shrink #${enhanced.shrinkAttempt} (finding minimal counterexample)`) +
27
+ `, value=${value}, total=${enhanced.totalExecutions}`,
28
+ )
29
+
30
+ // This test always passes, so no shrinking will occur
31
+ return
32
+ }),
33
+ { fastCheck: { numRuns: 4 } },
34
+ )
35
+
36
+ // Failing test - should show initial + shrinking phases
37
+ let alreadyFailed = false
38
+ Vitest.asProp(
39
+ Vitest.scopedLive,
40
+ 'failing test (shows initial runs + shrinking)',
41
+ [IntArbitrary],
42
+ (properties, ctx, enhanced) =>
43
+ Effect.gen(function* () {
44
+ const [value] = properties
45
+ if (value === undefined) {
46
+ return yield* Effect.fail(new Error('Value is undefined'))
47
+ }
48
+
49
+ const displayInfo =
50
+ enhanced._tag === 'initial'
51
+ ? `Run ${enhanced.runIndex + 1}/${enhanced.numRuns}`
52
+ : `Shrink #${enhanced.shrinkAttempt} (finding minimal counterexample)`
53
+
54
+ const status = value > 80 ? '💥' : '✅'
55
+ console.log(
56
+ `${status} FAILING-TEST [${enhanced._tag.toUpperCase()}]: ${displayInfo}, value=${value}, total=${enhanced.totalExecutions}`,
57
+ )
58
+
59
+ // Fail when value is greater than 80 to trigger shrinking
60
+ if (value > 80) {
61
+ alreadyFailed = true
62
+ return yield* Effect.fail(new Error(`Value ${value} is too large (> 80)`))
63
+ }
64
+
65
+ if (alreadyFailed && enhanced._tag === 'shrinking') {
66
+ ctx.skip("For the sake of this test, we don't want to fail but want to skip")
67
+ return
68
+ }
69
+
70
+ return
71
+ }),
72
+ { fastCheck: { numRuns: 3 } },
73
+ )
74
+
75
+ // Test with endOnFailure: true - should not show shrinking
76
+ Vitest.asProp(
77
+ Vitest.scopedLive,
78
+ 'failing test with endOnFailure (no shrinking)',
79
+ [IntArbitrary],
80
+ (properties, _ctx, enhanced) =>
81
+ Effect.gen(function* () {
82
+ const [value] = properties
83
+ if (value === undefined) {
84
+ return yield* Effect.fail(new Error('Value is undefined'))
85
+ }
86
+
87
+ console.log(
88
+ `🚫 END-ON-FAILURE [${enhanced._tag.toUpperCase()}]: ` +
89
+ `Run ${enhanced.runIndex + 1}/${enhanced.numRuns}, value=${value}, total=${enhanced.totalExecutions}`,
90
+ )
91
+
92
+ // This will fail but shrinking is disabled
93
+ if (value > 50) {
94
+ return yield* Effect.fail(new Error(`Value ${value} is too large (> 50) - but no shrinking!`))
95
+ }
96
+
97
+ return
98
+ }),
99
+ {
100
+ fastCheck: {
101
+ numRuns: 5,
102
+ endOnFailure: true,
103
+ // Provide explicit samples so the second run crosses >50. Randomly drawing five
104
+ // integers has ~3% chance to stay ≤ 50, which would break the `fails: true`
105
+ // expectation even though shrinking remains disabled. The examples keep the
106
+ // demo readable while leaving the remaining runs to FastCheck.
107
+ examples: [[5], [66]],
108
+ },
109
+ fails: true,
110
+ },
111
+ )
112
+ })
@@ -0,0 +1,238 @@
1
+ import * as inspector from 'node:inspector'
2
+ import type * as Vitest from '@effect/vitest'
3
+ import { IS_CI } from '@livestore/utils'
4
+ import {
5
+ type Cause,
6
+ Duration,
7
+ Effect,
8
+ type FastCheck as FC,
9
+ identity,
10
+ Layer,
11
+ type OtelTracer,
12
+ Predicate,
13
+ type Schema,
14
+ type Scope,
15
+ } from '@livestore/utils/effect'
16
+ import { OtelLiveDummy } from '@livestore/utils/node'
17
+ import { OtelLiveHttp } from '../node/mod.ts'
18
+
19
+ export * from '@effect/vitest'
20
+
21
+ export const DEBUGGER_ACTIVE = Boolean(process.env.DEBUGGER_ACTIVE ?? inspector.url() !== undefined)
22
+
23
+ export const makeWithTestCtx: <ROut = never, E1 = never, RIn = never>(
24
+ ctxParams: WithTestCtxParams<ROut, E1, RIn>,
25
+ ) => (testContext: Vitest.TestContext) => <A, E, R>(
26
+ self: Effect.Effect<A, E, R>,
27
+ ) => Effect.Effect<
28
+ A,
29
+ E | E1 | Cause.TimeoutException,
30
+ // Exclude dependencies provided by `withTestCtx` from the layer dependencies
31
+ | Exclude<RIn, OtelTracer.OtelTracer | Scope.Scope>
32
+ // Exclude dependencies provided by `withTestCtx` **and** dependencies produced
33
+ // by the layer from the effect dependencies
34
+ | Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
35
+ > = (ctxParams) => (testContext: Vitest.TestContext) => withTestCtx(testContext, ctxParams)
36
+
37
+ export type WithTestCtxParams<ROut, E1, RIn> = {
38
+ suffix?: string
39
+ makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn | Scope.Scope>
40
+ timeout?: Duration.DurationInput
41
+ forceOtel?: boolean
42
+ }
43
+
44
+ export const withTestCtx =
45
+ <ROut = never, E1 = never, RIn = never>(
46
+ testContext: Vitest.TestContext,
47
+ {
48
+ suffix,
49
+ makeLayer,
50
+ timeout = IS_CI ? 60_000 : 10_000,
51
+ forceOtel = false,
52
+ }: {
53
+ suffix?: string
54
+ makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn>
55
+ timeout?: Duration.DurationInput
56
+ forceOtel?: boolean
57
+ } = {},
58
+ ) =>
59
+ <A, E, R>(
60
+ self: Effect.Effect<A, E, R>,
61
+ ): Effect.Effect<
62
+ A,
63
+ E | E1 | Cause.TimeoutException,
64
+ // Exclude dependencies provided internally from the provided layer's dependencies
65
+ | Exclude<RIn, OtelTracer.OtelTracer | Scope.Scope>
66
+ // Exclude dependencies provided internally **and** dependencies produced by the
67
+ // provided layer from the effect dependencies
68
+ | Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
69
+ > => {
70
+ const spanName = `${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`
71
+ const layer = makeLayer?.(testContext) ?? Layer.empty
72
+
73
+ const otelLayer =
74
+ DEBUGGER_ACTIVE || forceOtel
75
+ ? OtelLiveHttp({ rootSpanName: spanName, serviceName: 'vitest-runner', skipLogUrl: false })
76
+ : OtelLiveDummy
77
+
78
+ const combinedLayer = layer.pipe(Layer.provideMerge(otelLayer))
79
+
80
+ return self.pipe(
81
+ DEBUGGER_ACTIVE
82
+ ? identity
83
+ : Effect.logWarnIfTakesLongerThan({
84
+ duration: Duration.toMillis(timeout) * 0.8,
85
+ label: `${spanName} approaching timeout (timeout: ${Duration.format(timeout)})`,
86
+ }),
87
+ DEBUGGER_ACTIVE ? identity : Effect.timeout(timeout),
88
+ Effect.provide(combinedLayer),
89
+ Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
90
+ Effect.annotateLogs({ suffix }),
91
+ ) as any
92
+ }
93
+
94
+ /**
95
+ * Shared properties for all enhanced test context phases
96
+ */
97
+ export interface EnhancedTestContextBase {
98
+ numRuns: number
99
+ /** 0-based index */
100
+ runIndex: number
101
+ /** Total number of executions including initial runs and shrinking attempts */
102
+ totalExecutions: number
103
+ }
104
+
105
+ /**
106
+ * Enhanced context for property-based tests that includes shrinking phase information
107
+ */
108
+ export type EnhancedTestContext =
109
+ | (EnhancedTestContextBase & {
110
+ _tag: 'initial'
111
+ })
112
+ | (EnhancedTestContextBase & {
113
+ _tag: 'shrinking'
114
+ /** Number of shrinking attempts */
115
+ shrinkAttempt: number
116
+ })
117
+
118
+ /**
119
+ * Normalizes propOptions to ensure @effect/vitest receives correct fastCheck structure
120
+ */
121
+ const normalizePropOptions = <Arbs extends Vitest.Vitest.Arbitraries>(
122
+ propOptions:
123
+ | number
124
+ | (Vitest.TestOptions & {
125
+ fastCheck?: FC.Parameters<{
126
+ [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]>
127
+ }>
128
+ }),
129
+ ): Vitest.TestOptions & {
130
+ fastCheck?: FC.Parameters<{
131
+ [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]>
132
+ }>
133
+ } => {
134
+ // If it's a number, treat as timeout and add our default fastCheck
135
+ if (!Predicate.isObject(propOptions)) {
136
+ return {
137
+ timeout: propOptions,
138
+ fastCheck: { numRuns: 100 },
139
+ }
140
+ }
141
+
142
+ // If no fastCheck property, add it with our default numRuns
143
+ if (!propOptions.fastCheck) {
144
+ return {
145
+ ...propOptions,
146
+ fastCheck: { numRuns: 100 },
147
+ }
148
+ }
149
+
150
+ // If fastCheck exists but no numRuns, add our default
151
+ if (propOptions.fastCheck && !propOptions.fastCheck.numRuns) {
152
+ return {
153
+ ...propOptions,
154
+ fastCheck: {
155
+ ...propOptions.fastCheck,
156
+ numRuns: 100,
157
+ },
158
+ }
159
+ }
160
+
161
+ // If everything is properly structured, pass through
162
+ return propOptions
163
+ }
164
+
165
+ /**
166
+ * Equivalent to Vitest.prop but provides enhanced context including shrinking progress visibility
167
+ *
168
+ * This function enhances the standard property-based testing by providing clear information about
169
+ * whether FastCheck is in the initial testing phase or the shrinking phase, solving the confusion
170
+ * where tests show "Run 26/6" when FastCheck's shrinking algorithm is active.
171
+ *
172
+ * TODO: allow for upper timelimit instead of / additional to `numRuns`
173
+ *
174
+ * TODO: Upstream to Effect
175
+ */
176
+ export const asProp = <Arbs extends Vitest.Vitest.Arbitraries, A, E, R>(
177
+ api: Vitest.Vitest.Tester<R>,
178
+ name: string,
179
+ arbitraries: Arbs,
180
+ test: Vitest.Vitest.TestFunction<
181
+ A,
182
+ E,
183
+ R,
184
+ [
185
+ { [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]> },
186
+ Vitest.TestContext,
187
+ EnhancedTestContext,
188
+ ]
189
+ >,
190
+ propOptions:
191
+ | number
192
+ | (Vitest.TestOptions & {
193
+ fastCheck?: FC.Parameters<{
194
+ [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]>
195
+ }>
196
+ }),
197
+ ) => {
198
+ const normalizedPropOptions = normalizePropOptions(propOptions)
199
+ const numRuns = normalizedPropOptions.fastCheck?.numRuns ?? 100
200
+ let runIndex = 0
201
+ let shrinkAttempts = 0
202
+ let totalExecutions = 0
203
+
204
+ return api.prop(
205
+ name,
206
+ arbitraries,
207
+ (properties, ctx) => {
208
+ if (ctx.signal.aborted) {
209
+ return ctx.skip('Test aborted')
210
+ }
211
+
212
+ totalExecutions++
213
+ const isInShrinkingPhase = runIndex >= numRuns
214
+
215
+ if (isInShrinkingPhase) {
216
+ shrinkAttempts++
217
+ }
218
+
219
+ const enhancedContext: EnhancedTestContext = isInShrinkingPhase
220
+ ? {
221
+ _tag: 'shrinking',
222
+ numRuns,
223
+ runIndex: runIndex++,
224
+ shrinkAttempt: shrinkAttempts,
225
+ totalExecutions,
226
+ }
227
+ : {
228
+ _tag: 'initial',
229
+ numRuns,
230
+ runIndex: runIndex++,
231
+ totalExecutions,
232
+ }
233
+
234
+ return test(properties, ctx, enhancedContext)
235
+ },
236
+ normalizedPropOptions,
237
+ )
238
+ }
@@ -1 +1,3 @@
1
- export * as Vitest from '@effect/vitest'
1
+ import './global.ts'
2
+
3
+ export * as Vitest from './Vitest.ts'