test: add live effect helper mode

Default the shared effect test helper to support both test-clock and live execution, and switch the current opencode effect tests to the live path for real integration behavior.
pull/20304/head
Kit Langton 2026-03-31 11:44:47 -04:00
parent 459fbc99a8
commit 6ea467b0ac
10 changed files with 106 additions and 90 deletions

View File

@ -16,21 +16,21 @@ const truncate = Layer.effectDiscard(
const it = testEffect(Layer.merge(AccountRepo.layer, truncate))
it.effect("list returns empty when no accounts exist", () =>
it.live("list returns empty when no accounts exist", () =>
Effect.gen(function* () {
const accounts = yield* AccountRepo.use((r) => r.list())
expect(accounts).toEqual([])
}),
)
it.effect("active returns none when no accounts exist", () =>
it.live("active returns none when no accounts exist", () =>
Effect.gen(function* () {
const active = yield* AccountRepo.use((r) => r.active())
expect(Option.isNone(active)).toBe(true)
}),
)
it.effect("persistAccount inserts and getRow retrieves", () =>
it.live("persistAccount inserts and getRow retrieves", () =>
Effect.gen(function* () {
const id = AccountID.make("user-1")
yield* AccountRepo.use((r) =>
@ -56,7 +56,7 @@ it.effect("persistAccount inserts and getRow retrieves", () =>
}),
)
it.effect("persistAccount sets the active account and org", () =>
it.live("persistAccount sets the active account and org", () =>
Effect.gen(function* () {
const id1 = AccountID.make("user-1")
const id2 = AccountID.make("user-2")
@ -93,7 +93,7 @@ it.effect("persistAccount sets the active account and org", () =>
}),
)
it.effect("list returns all accounts", () =>
it.live("list returns all accounts", () =>
Effect.gen(function* () {
const id1 = AccountID.make("user-1")
const id2 = AccountID.make("user-2")
@ -128,7 +128,7 @@ it.effect("list returns all accounts", () =>
}),
)
it.effect("remove deletes an account", () =>
it.live("remove deletes an account", () =>
Effect.gen(function* () {
const id = AccountID.make("user-1")
@ -151,7 +151,7 @@ it.effect("remove deletes an account", () =>
}),
)
it.effect("use stores the selected org and marks the account active", () =>
it.live("use stores the selected org and marks the account active", () =>
Effect.gen(function* () {
const id1 = AccountID.make("user-1")
const id2 = AccountID.make("user-2")
@ -191,7 +191,7 @@ it.effect("use stores the selected org and marks the account active", () =>
}),
)
it.effect("persistToken updates token fields", () =>
it.live("persistToken updates token fields", () =>
Effect.gen(function* () {
const id = AccountID.make("user-1")
@ -225,7 +225,7 @@ it.effect("persistToken updates token fields", () =>
}),
)
it.effect("persistToken with no expiry sets token_expiry to null", () =>
it.live("persistToken with no expiry sets token_expiry to null", () =>
Effect.gen(function* () {
const id = AccountID.make("user-1")
@ -255,7 +255,7 @@ it.effect("persistToken with no expiry sets token_expiry to null", () =>
}),
)
it.effect("persistAccount upserts on conflict", () =>
it.live("persistAccount upserts on conflict", () =>
Effect.gen(function* () {
const id = AccountID.make("user-1")
@ -295,7 +295,7 @@ it.effect("persistAccount upserts on conflict", () =>
}),
)
it.effect("remove clears active state when deleting the active account", () =>
it.live("remove clears active state when deleting the active account", () =>
Effect.gen(function* () {
const id = AccountID.make("user-1")
@ -318,7 +318,7 @@ it.effect("remove clears active state when deleting the active account", () =>
}),
)
it.effect("getRow returns none for nonexistent account", () =>
it.live("getRow returns none for nonexistent account", () =>
Effect.gen(function* () {
const row = yield* AccountRepo.use((r) => r.getRow(AccountID.make("nope")))
expect(Option.isNone(row)).toBe(true)

View File

@ -54,7 +54,7 @@ const deviceTokenClient = (body: unknown, status = 400) =>
const poll = (body: unknown, status = 400) =>
Account.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(deviceTokenClient(body, status))))
it.effect("orgsByAccount groups orgs per account", () =>
it.live("orgsByAccount groups orgs per account", () =>
Effect.gen(function* () {
yield* AccountRepo.use((r) =>
r.persistAccount({
@ -107,7 +107,7 @@ it.effect("orgsByAccount groups orgs per account", () =>
}),
)
it.effect("token refresh persists the new token", () =>
it.live("token refresh persists the new token", () =>
Effect.gen(function* () {
const id = AccountID.make("user-1")
@ -148,7 +148,7 @@ it.effect("token refresh persists the new token", () =>
}),
)
it.effect("config sends the selected org header", () =>
it.live("config sends the selected org header", () =>
Effect.gen(function* () {
const id = AccountID.make("user-1")
@ -188,7 +188,7 @@ it.effect("config sends the selected org header", () =>
}),
)
it.effect("poll stores the account and first org on success", () =>
it.live("poll stores the account and first org on success", () =>
Effect.gen(function* () {
const client = HttpClient.make((req) =>
Effect.succeed(
@ -259,7 +259,7 @@ for (const [name, body, expectedTag] of [
"PollExpired",
],
] as const) {
it.effect(`poll returns ${name} for ${body.error}`, () =>
it.live(`poll returns ${name} for ${body.error}`, () =>
Effect.gen(function* () {
const result = yield* poll(body)
expect(result._tag).toBe(expectedTag)
@ -267,7 +267,7 @@ for (const [name, body, expectedTag] of [
)
}
it.effect("poll returns poll error for other OAuth errors", () =>
it.live("poll returns poll error for other OAuth errors", () =>
Effect.gen(function* () {
const result = yield* poll({
error: "server_error",

View File

@ -22,7 +22,7 @@ const live = Layer.mergeAll(Bus.layer, node)
const it = testEffect(live)
describe("Bus (Effect-native)", () => {
it.effect("publish + subscribe stream delivers events", () =>
it.live("publish + subscribe stream delivers events", () =>
provideTmpdirInstance(() =>
Effect.gen(function* () {
const bus = yield* Bus.Service
@ -46,7 +46,7 @@ describe("Bus (Effect-native)", () => {
),
)
it.effect("subscribe filters by event type", () =>
it.live("subscribe filters by event type", () =>
provideTmpdirInstance(() =>
Effect.gen(function* () {
const bus = yield* Bus.Service
@ -70,7 +70,7 @@ describe("Bus (Effect-native)", () => {
),
)
it.effect("subscribeAll receives all types", () =>
it.live("subscribeAll receives all types", () =>
provideTmpdirInstance(() =>
Effect.gen(function* () {
const bus = yield* Bus.Service
@ -95,7 +95,7 @@ describe("Bus (Effect-native)", () => {
),
)
it.effect("multiple subscribers each receive the event", () =>
it.live("multiple subscribers each receive the event", () =>
provideTmpdirInstance(() =>
Effect.gen(function* () {
const bus = yield* Bus.Service
@ -129,7 +129,7 @@ describe("Bus (Effect-native)", () => {
),
)
it.effect("subscribeAll stream sees InstanceDisposed on disposal", () =>
it.live("subscribeAll stream sees InstanceDisposed on disposal", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const types: string[] = []

View File

@ -6,7 +6,7 @@ import { it } from "../lib/effect"
describe("Runner", () => {
// --- ensureRunning semantics ---
it.effect(
it.live(
"ensureRunning starts work and returns result",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -18,7 +18,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"ensureRunning propagates work failures",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -29,7 +29,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"concurrent callers share the same run",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -51,7 +51,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"concurrent callers all receive same error",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -71,7 +71,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"ensureRunning can be called again after previous run completes",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -81,7 +81,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"second ensureRunning ignores new work if already running",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -110,7 +110,7 @@ describe("Runner", () => {
// --- cancel semantics ---
it.effect(
it.live(
"cancel interrupts running work",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -128,7 +128,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"cancel on idle is a no-op",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -138,7 +138,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"cancel with onInterrupt resolves callers gracefully",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -154,7 +154,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"cancel with queued callers resolves all",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -175,7 +175,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"work can be started after cancel",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -245,7 +245,7 @@ describe("Runner", () => {
// --- shell semantics ---
it.effect(
it.live(
"shell runs exclusively",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -256,7 +256,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"shell rejects when run is active",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -272,7 +272,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"shell rejects when another shell is running",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -292,7 +292,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"shell rejects via busy callback and cancel still stops the first shell",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -323,7 +323,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"cancel interrupts shell that ignores abort signal",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -349,7 +349,7 @@ describe("Runner", () => {
// --- shell→run handoff ---
it.effect(
it.live(
"ensureRunning queues behind shell then runs after",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -376,7 +376,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"multiple ensureRunning callers share the queued run behind shell",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -407,7 +407,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"cancel during shell_then_run cancels both",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -441,7 +441,7 @@ describe("Runner", () => {
// --- lifecycle callbacks ---
it.effect(
it.live(
"onIdle fires when returning to idle from running",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -454,7 +454,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"onIdle fires on cancel",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -470,7 +470,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"onBusy fires when shell starts",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -485,7 +485,7 @@ describe("Runner", () => {
// --- busy flag ---
it.effect(
it.live(
"busy is true during run",
Effect.gen(function* () {
const s = yield* Scope.Scope
@ -502,7 +502,7 @@ describe("Runner", () => {
}),
)
it.effect(
it.live(
"busy is true during shell",
Effect.gen(function* () {
const s = yield* Scope.Scope

View File

@ -10,7 +10,7 @@ import * as Formatter from "../../src/format/formatter"
const it = testEffect(Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer))
describe("Format", () => {
it.effect("status() returns built-in formatters when no config overrides", () =>
it.live("status() returns built-in formatters when no config overrides", () =>
provideTmpdirInstance(() =>
Format.Service.use((fmt) =>
Effect.gen(function* () {
@ -32,7 +32,7 @@ describe("Format", () => {
),
)
it.effect("status() returns empty list when formatter is disabled", () =>
it.live("status() returns empty list when formatter is disabled", () =>
provideTmpdirInstance(
() =>
Format.Service.use((fmt) =>
@ -44,7 +44,7 @@ describe("Format", () => {
),
)
it.effect("status() excludes formatters marked as disabled in config", () =>
it.live("status() excludes formatters marked as disabled in config", () =>
provideTmpdirInstance(
() =>
Format.Service.use((fmt) =>
@ -64,11 +64,11 @@ describe("Format", () => {
),
)
it.effect("service initializes without error", () =>
it.live("service initializes without error", () =>
provideTmpdirInstance(() => Format.Service.use(() => Effect.void)),
)
it.effect("status() initializes formatter state per directory", () =>
it.live("status() initializes formatter state per directory", () =>
Effect.gen(function* () {
const a = yield* provideTmpdirInstance(() => Format.Service.use((fmt) => fmt.status()), {
config: { formatter: false },
@ -80,7 +80,7 @@ describe("Format", () => {
}),
)
it.effect("runs enabled checks for matching formatters in parallel", () =>
it.live("runs enabled checks for matching formatters in parallel", () =>
provideTmpdirInstance((path) =>
Effect.gen(function* () {
const file = `${path}/test.parallel`
@ -144,7 +144,7 @@ describe("Format", () => {
),
)
it.effect("runs matching formatters sequentially for the same file", () =>
it.live("runs matching formatters sequentially for the same file", () =>
provideTmpdirInstance(
(path) =>
Effect.gen(function* () {

View File

@ -1,10 +1,10 @@
import { test, type TestOptions } from "bun:test"
import { Cause, Effect, Exit, Layer } from "effect"
import type * as Scope from "effect/Scope"
import * as TestClock from "effect/testing/TestClock"
import * as TestConsole from "effect/testing/TestConsole"
type Body<A, E, R> = Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)
const env = TestConsole.layer
const body = <A, E, R>(value: Body<A, E, R>) => Effect.suspend(() => (typeof value === "function" ? value() : value))
@ -19,19 +19,35 @@ const run = <A, E, R, E2>(value: Body<A, E, R | Scope.Scope>, layer: Layer.Layer
return yield* exit
}).pipe(Effect.runPromise)
const make = <R, E>(layer: Layer.Layer<R, E, never>) => {
const make = <R, E>(testLayer: Layer.Layer<R, E, never>, liveLayer: Layer.Layer<R, E, never>) => {
const effect = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
test(name, () => run(value, layer), opts)
test(name, () => run(value, testLayer), opts)
effect.only = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
test.only(name, () => run(value, layer), opts)
test.only(name, () => run(value, testLayer), opts)
effect.skip = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
test.skip(name, () => run(value, layer), opts)
test.skip(name, () => run(value, testLayer), opts)
return { effect }
const live = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
test(name, () => run(value, liveLayer), opts)
live.only = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
test.only(name, () => run(value, liveLayer), opts)
live.skip = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
test.skip(name, () => run(value, liveLayer), opts)
return { effect, live }
}
export const it = make(env)
// Test environment with TestClock and TestConsole
const testEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer())
export const testEffect = <R, E>(layer: Layer.Layer<R, E, never>) => make(Layer.provideMerge(layer, env))
// Live environment - uses real clock, but keeps TestConsole for output capture
const liveEnv = TestConsole.layer
export const it = make(testEnv, liveEnv)
export const testEffect = <R, E>(layer: Layer.Layer<R, E, never>) =>
make(Layer.provideMerge(layer, testEnv), Layer.provideMerge(layer, liveEnv))

View File

@ -264,7 +264,7 @@ const env = SessionProcessor.layer.pipe(Layer.provideMerge(deps))
const it = testEffect(env)
it.effect("session.processor effect tests capture llm input cleanly", () => {
it.live("session.processor effect tests capture llm input cleanly", () => {
return provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -316,7 +316,7 @@ it.effect("session.processor effect tests capture llm input cleanly", () => {
)
})
it.effect("session.processor effect tests stop after token overflow requests compaction", () => {
it.live("session.processor effect tests stop after token overflow requests compaction", () => {
return provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -376,7 +376,7 @@ it.effect("session.processor effect tests stop after token overflow requests com
)
})
it.effect("session.processor effect tests reset reasoning state across retries", () => {
it.live("session.processor effect tests reset reasoning state across retries", () => {
return provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -449,7 +449,7 @@ it.effect("session.processor effect tests reset reasoning state across retries",
)
})
it.effect("session.processor effect tests do not retry unknown json errors", () => {
it.live("session.processor effect tests do not retry unknown json errors", () => {
return provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -495,7 +495,7 @@ it.effect("session.processor effect tests do not retry unknown json errors", ()
)
})
it.effect("session.processor effect tests retry recognized structured json errors", () => {
it.live("session.processor effect tests retry recognized structured json errors", () => {
return provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -544,7 +544,7 @@ it.effect("session.processor effect tests retry recognized structured json error
)
})
it.effect("session.processor effect tests publish retry status updates", () => {
it.live("session.processor effect tests publish retry status updates", () => {
return provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -611,7 +611,7 @@ it.effect("session.processor effect tests publish retry status updates", () => {
)
})
it.effect("session.processor effect tests compact on structured context overflow", () => {
it.live("session.processor effect tests compact on structured context overflow", () => {
return provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -656,7 +656,7 @@ it.effect("session.processor effect tests compact on structured context overflow
)
})
it.effect("session.processor effect tests mark pending tools as aborted on cleanup", () => {
it.live("session.processor effect tests mark pending tools as aborted on cleanup", () => {
return provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -725,7 +725,7 @@ it.effect("session.processor effect tests mark pending tools as aborted on clean
)
})
it.effect("session.processor effect tests record aborted errors and idle state", () => {
it.live("session.processor effect tests record aborted errors and idle state", () => {
return provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -807,7 +807,7 @@ it.effect("session.processor effect tests record aborted errors and idle state",
)
})
it.effect("session.processor effect tests mark interruptions aborted without manual abort", () => {
it.live("session.processor effect tests mark interruptions aborted without manual abort", () => {
return provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {

View File

@ -416,7 +416,7 @@ const boot = Effect.fn("test.boot")(function* (input?: { title?: string }) {
// Loop semantics
it.effect("loop exits immediately when last assistant has stop finish", () =>
it.live("loop exits immediately when last assistant has stop finish", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -432,7 +432,7 @@ it.effect("loop exits immediately when last assistant has stop finish", () =>
),
)
it.effect("loop calls LLM and returns assistant message", () =>
it.live("loop calls LLM and returns assistant message", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -450,7 +450,7 @@ it.effect("loop calls LLM and returns assistant message", () =>
),
)
it.effect("loop continues when finish is tool-calls", () =>
it.live("loop continues when finish is tool-calls", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -471,7 +471,7 @@ it.effect("loop continues when finish is tool-calls", () =>
),
)
it.effect("failed subtask preserves metadata on error tool state", () =>
it.live("failed subtask preserves metadata on error tool state", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -532,7 +532,7 @@ it.effect("failed subtask preserves metadata on error tool state", () =>
),
)
it.effect("loop sets status to busy then idle", () =>
it.live("loop sets status to busy then idle", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -567,7 +567,7 @@ it.effect("loop sets status to busy then idle", () =>
// Cancel semantics
it.effect(
it.live(
"cancel interrupts loop and resolves with an assistant message",
() =>
provideTmpdirInstance(
@ -598,7 +598,7 @@ it.effect(
30_000,
)
it.effect(
it.live(
"cancel records MessageAbortedError on interrupted process",
() =>
provideTmpdirInstance(
@ -632,7 +632,7 @@ it.effect(
30_000,
)
it.effect(
it.live(
"cancel finalizes subtask tool state",
() =>
provideTmpdirInstance(
@ -695,7 +695,7 @@ it.effect(
30_000,
)
it.effect(
it.live(
"cancel with queued callers resolves all cleanly",
() =>
provideTmpdirInstance(
@ -733,7 +733,7 @@ it.effect(
// Queue semantics
it.effect("concurrent loop callers get same result", () =>
it.live("concurrent loop callers get same result", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -752,7 +752,7 @@ it.effect("concurrent loop callers get same result", () =>
),
)
it.effect("concurrent loop callers all receive same error result", () =>
it.live("concurrent loop callers all receive same error result", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -780,7 +780,7 @@ it.effect("concurrent loop callers all receive same error result", () =>
),
)
it.effect(
it.live(
"prompt submitted during an active run is included in the next LLM input",
() =>
provideTmpdirInstance(
@ -860,7 +860,7 @@ it.effect(
30_000,
)
it.effect(
it.live(
"assertNotBusy throws BusyError when loop running",
() =>
provideTmpdirInstance(
@ -897,7 +897,7 @@ it.effect(
30_000,
)
it.effect("assertNotBusy succeeds when idle", () =>
it.live("assertNotBusy succeeds when idle", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
@ -914,7 +914,7 @@ it.effect("assertNotBusy succeeds when idle", () =>
// Shell semantics
it.effect(
it.live(
"shell rejects with BusyError when loop running",
() =>
provideTmpdirInstance(

View File

@ -52,7 +52,7 @@ function makeConfig(url: string) {
}
describe("session.prompt provider integration", () => {
it.effect("loop returns assistant text through local provider", () =>
it.live("loop returns assistant text through local provider", () =>
Effect.gen(function* () {
const llm = yield* TestLLMServer
return yield* provideTmpdirInstance(
@ -87,7 +87,7 @@ describe("session.prompt provider integration", () => {
}),
)
it.effect("loop consumes queued replies across turns", () =>
it.live("loop consumes queued replies across turns", () =>
Effect.gen(function* () {
const llm = yield* TestLLMServer
return yield* provideTmpdirInstance(

View File

@ -140,7 +140,7 @@ describe("Truncate", () => {
const DAY_MS = 24 * 60 * 60 * 1000
const it = testEffect(Layer.mergeAll(TruncateSvc.defaultLayer, NodeFileSystem.layer))
it.effect("deletes files older than 7 days and preserves recent files", () =>
it.live("deletes files older than 7 days and preserves recent files", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem