diff --git a/src/testing/module-mock-lifecycle.test.ts b/src/testing/module-mock-lifecycle.test.ts
new file mode 100644
index 000000000..7a7c210f3
--- /dev/null
+++ b/src/testing/module-mock-lifecycle.test.ts
@@ -0,0 +1,82 @@
+///
+
+import { describe, expect, mock, test } from "bun:test"
+import { installModuleMockLifecycle } from "./module-mock-lifecycle"
+
+describe("installModuleMockLifecycle", () => {
+ test("restores the original module exports on mock.restore", () => {
+ // given
+ const moduleCalls: Array<{ specifier: string; value: Record }> = []
+ const mockApi = {
+ module: (specifier: string, factory: () => Record) => {
+ moduleCalls.push({ specifier, value: factory() })
+ },
+ restore: mock(() => {}),
+ }
+
+ installModuleMockLifecycle(mockApi, {
+ getCallerUrl: () => "file:///repo/tests/example.test.ts",
+ resolveSpecifier: (specifier) => `resolved:${specifier}`,
+ loadOriginalModule: () => ({ ok: true, value: { named: "original" } }),
+ })
+
+ // when
+ mockApi.module("./dependency", () => ({ named: "mocked" }))
+ mockApi.restore()
+
+ // then
+ expect(moduleCalls).toEqual([
+ { specifier: "./dependency", value: { named: "mocked" } },
+ { specifier: "resolved:./dependency", value: { named: "original" } },
+ ])
+ })
+
+ test("captures the original module only once per resolved specifier", () => {
+ // given
+ let loadCount = 0
+ const mockApi = {
+ module: mock(() => {}),
+ restore: mock(() => {}),
+ }
+
+ installModuleMockLifecycle(mockApi, {
+ getCallerUrl: () => "file:///repo/tests/example.test.ts",
+ resolveSpecifier: () => "file:///repo/src/dependency.ts",
+ loadOriginalModule: () => {
+ loadCount += 1
+ return { ok: true, value: { named: "original" } }
+ },
+ })
+
+ // when
+ mockApi.module("./dependency", () => ({ named: "first" }))
+ mockApi.module("./dependency", () => ({ named: "second" }))
+
+ // then
+ expect(loadCount).toBe(1)
+ })
+
+ test("does not restore unresolved modules to avoid cleanup errors", () => {
+ // given
+ const moduleCalls: Array<{ specifier: string; value: Record }> = []
+ const mockApi = {
+ module: (specifier: string, factory: () => Record) => {
+ moduleCalls.push({ specifier, value: factory() })
+ },
+ restore: mock(() => {}),
+ }
+
+ installModuleMockLifecycle(mockApi, {
+ getCallerUrl: () => "file:///repo/tests/example.test.ts",
+ resolveSpecifier: (specifier) => specifier,
+ loadOriginalModule: () => ({ ok: false, error: new Error("Cannot find module") }),
+ })
+
+ // when
+ mockApi.module("virtual:missing", () => ({ named: "mocked" }))
+ mockApi.restore()
+
+ // then - only the original mock call, no restore call for unresolved module
+ expect(moduleCalls).toEqual([{ specifier: "virtual:missing", value: { named: "mocked" } }])
+ })
+})
diff --git a/src/testing/module-mock-lifecycle.ts b/src/testing/module-mock-lifecycle.ts
new file mode 100644
index 000000000..d9b549eb1
--- /dev/null
+++ b/src/testing/module-mock-lifecycle.ts
@@ -0,0 +1,143 @@
+import { createRequire } from "node:module"
+import { pathToFileURL } from "node:url"
+
+type MockModuleFactory = () => Record
+
+type MockApi = {
+ module: (specifier: string, factory: MockModuleFactory) => unknown
+ restore: () => unknown
+}
+
+type ModuleLoadResult =
+ | { ok: true; value: unknown }
+ | { ok: false; error: Error }
+
+type ModuleSnapshot = {
+ restoreSpecifier: string
+ restoreFactory: MockModuleFactory
+}
+
+type ModuleMockLifecycleOptions = {
+ getCallerUrl?: () => string
+ resolveSpecifier?: (specifier: string, callerUrl: string) => string
+ loadOriginalModule?: (specifier: string, callerUrl: string) => ModuleLoadResult
+}
+
+function toError(error: unknown): Error {
+ if (error instanceof Error) {
+ return error
+ }
+
+ return new Error(String(error))
+}
+
+function cloneModuleExports(moduleValue: unknown): Record {
+ if (typeof moduleValue === "function") {
+ const functionExports = Object.assign({}, moduleValue)
+ return {
+ ...functionExports,
+ default: moduleValue,
+ }
+ }
+
+ if (moduleValue && typeof moduleValue === "object") {
+ return { ...(moduleValue as Record) }
+ }
+
+ return { default: moduleValue }
+}
+
+function normalizeStackPath(rawPath: string): string {
+ if (rawPath.startsWith("file://")) {
+ return rawPath
+ }
+
+ return pathToFileURL(rawPath).href
+}
+
+function defaultGetCallerUrl(): string {
+ const stack = new Error().stack ?? ""
+ const lines = stack.split("\n")
+
+ for (const line of lines) {
+ const match = line.match(/(?:\()?(file:\/\/[^\s)]+|\/[^\s):]+):(\d+):(\d+)/)
+ const candidatePath = match?.[1]
+ if (!candidatePath) {
+ continue
+ }
+
+ if (
+ candidatePath.includes("/test-setup.ts") ||
+ candidatePath.includes("/src/testing/module-mock-lifecycle.ts")
+ ) {
+ continue
+ }
+
+ return normalizeStackPath(candidatePath)
+ }
+
+ return import.meta.url
+}
+
+function defaultResolveSpecifier(specifier: string, callerUrl: string): string {
+ try {
+ return import.meta.resolve(specifier, callerUrl)
+ } catch {
+ return specifier
+ }
+}
+
+function defaultLoadOriginalModule(specifier: string, callerUrl: string): ModuleLoadResult {
+ try {
+ const require = createRequire(callerUrl)
+ return { ok: true, value: require(specifier) }
+ } catch (error) {
+ return { ok: false, error: toError(error) }
+ }
+}
+
+export function installModuleMockLifecycle(
+ mockApi: MockApi,
+ options: ModuleMockLifecycleOptions = {},
+): { restoreModuleMocks: () => void } {
+ const snapshots = new Map()
+ const delegateModule = mockApi.module.bind(mockApi)
+ const delegateRestore = mockApi.restore.bind(mockApi)
+ const getCallerUrl = options.getCallerUrl ?? defaultGetCallerUrl
+ const resolveSpecifier = options.resolveSpecifier ?? defaultResolveSpecifier
+ const loadOriginalModule = options.loadOriginalModule ?? defaultLoadOriginalModule
+
+ function restoreModuleMocks(): void {
+ for (const snapshot of snapshots.values()) {
+ delegateModule(snapshot.restoreSpecifier, snapshot.restoreFactory)
+ }
+
+ snapshots.clear()
+ }
+
+ mockApi.module = (specifier: string, factory: MockModuleFactory): unknown => {
+ const callerUrl = getCallerUrl()
+ const restoreSpecifier = resolveSpecifier(specifier, callerUrl)
+
+ if (!snapshots.has(restoreSpecifier)) {
+ const originalModule = loadOriginalModule(specifier, callerUrl)
+
+ if (originalModule.ok) {
+ const clonedExports = cloneModuleExports(originalModule.value)
+ snapshots.set(restoreSpecifier, {
+ restoreSpecifier,
+ restoreFactory: () => ({ ...clonedExports }),
+ })
+ }
+ }
+
+ return delegateModule(specifier, factory)
+ }
+
+ mockApi.restore = (): unknown => {
+ restoreModuleMocks()
+ return delegateRestore()
+ }
+
+ return { restoreModuleMocks }
+}