test(core): simplify mutation lock coverage

This commit is contained in:
Kit Langton
2026-08-07 23:44:17 -04:00
parent e7bd4f17c0
commit ea1c3b9a97
+25 -109
View File
@@ -87,7 +87,11 @@ const reset = () => {
denyAction = undefined
}
const withTool = <A, E, R>(directory: string, body: (registry: Tool.Interface) => Effect.Effect<A, E, R>) => {
const withTool = <A, E, R>(
directory: string,
body: (registry: Tool.Interface) => Effect.Effect<A, E, R>,
options?: { edit?: boolean },
) => {
const activeLocation = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
@@ -97,7 +101,13 @@ const withTool = <A, E, R>(directory: string, body: (registry: Tool.Interface) =
}).pipe(
Effect.provide(
AppNodeBuilder.build(
LayerNode.group([Tool.node, Tool.node, LocationMutation.node, FileMutation.node, writeToolNode]),
LayerNode.group([
Tool.node,
LocationMutation.node,
FileMutation.node,
writeToolNode,
...(options?.edit ? [editToolNode] : []),
]),
[
[
Environment.node,
@@ -115,28 +125,6 @@ const withTool = <A, E, R>(directory: string, body: (registry: Tool.Interface) =
)
}
const withMutationTools = <A, E, R>(directory: string, body: (registry: Tool.Interface) => Effect.Effect<A, E, R>) => {
const activeLocation = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
)
return Effect.gen(function* () {
return yield* body(yield* Tool.Service)
}).pipe(
Effect.provide(
AppNodeBuilder.build(
LayerNode.group([Tool.node, Tool.node, LocationMutation.node, FileMutation.node, writeToolNode, editToolNode]),
[
[Environment.node, environment],
[Location.node, activeLocation],
[Formatter.node, formatter],
[Permission.node, permission],
],
),
),
)
}
const call = (input: typeof WriteTool.Input.Type, id = "call-write") => ({
sessionID,
...toolIdentity,
@@ -483,15 +471,21 @@ describe("WriteTool", () => {
? Deferred.succeed(editApproved, undefined).pipe(Effect.asVoid)
: Effect.void
const write = yield* withMutationTools(tmp.path, (registry) =>
executeTool(registry, call({ path: "shared.txt", content: "before" }, "call-serialized-write")),
const write = yield* withTool(
tmp.path,
(registry) =>
executeTool(registry, call({ path: "shared.txt", content: "before" }, "call-serialized-write")),
{ edit: true },
).pipe(Effect.forkChild)
yield* Deferred.await(formatting)
const edit = yield* withMutationTools(tmp.path, (registry) =>
executeTool(
registry,
editCall({ path: "shared.txt", oldString: "before", newString: "after" }, "call-serialized-edit"),
),
const edit = yield* withTool(
tmp.path,
(registry) =>
executeTool(
registry,
editCall({ path: "shared.txt", oldString: "before", newString: "after" }, "call-serialized-edit"),
),
{ edit: true },
).pipe(Effect.forkChild)
yield* Effect.yieldNow
expect(yield* Deferred.isDone(editApproved)).toBe(false)
@@ -507,82 +501,4 @@ describe("WriteTool", () => {
),
)
it.live("serializes complete write transactions across Location service instances", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) => {
reset()
const target = path.join(tmp.path, "shared.txt")
return Effect.gen(function* () {
const formatting = yield* Deferred.make<void>()
const releaseFormatting = yield* Deferred.make<void>()
const secondApproved = yield* Deferred.make<void>()
let formats = 0
formatFile = () =>
++formats === 1
? Deferred.succeed(formatting, undefined).pipe(
Effect.andThen(Deferred.await(releaseFormatting)),
Effect.as(false),
)
: Effect.succeed(false)
afterPermission = (input) =>
input.source?.id === "call-second-write" && input.action === "edit"
? Deferred.succeed(secondApproved, undefined).pipe(Effect.asVoid)
: Effect.void
const first = yield* withTool(tmp.path, (registry) =>
executeTool(registry, call({ path: "shared.txt", content: "first" }, "call-first-write")),
).pipe(Effect.forkChild)
yield* Deferred.await(formatting)
const second = yield* withTool(tmp.path, (registry) =>
executeTool(registry, call({ path: "shared.txt", content: "second" }, "call-second-write")),
).pipe(Effect.forkChild)
yield* Effect.yieldNow
expect(yield* Deferred.isDone(secondApproved)).toBe(false)
yield* Deferred.succeed(releaseFormatting, undefined)
expect((yield* Fiber.join(first)).status).toBe("completed")
expect((yield* Fiber.join(second)).status).toBe("completed")
expect(yield* Deferred.isDone(secondApproved)).toBe(true)
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("second")
})
},
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
it.live("allows complete write transactions for unrelated paths to run concurrently", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) => {
reset()
return Effect.gen(function* () {
const formatting = yield* Deferred.make<void>()
const releaseFormatting = yield* Deferred.make<void>()
let formats = 0
formatFile = () =>
++formats === 1
? Deferred.succeed(formatting, undefined).pipe(
Effect.andThen(Deferred.await(releaseFormatting)),
Effect.as(false),
)
: Effect.succeed(false)
const first = yield* withTool(tmp.path, (registry) =>
executeTool(registry, call({ path: "first.txt", content: "first" }, "call-first-path")),
).pipe(Effect.forkChild)
yield* Deferred.await(formatting)
const second = yield* withTool(tmp.path, (registry) =>
executeTool(registry, call({ path: "second.txt", content: "second" }, "call-second-path")),
).pipe(Effect.forkChild)
expect((yield* Fiber.join(second)).status).toBe("completed")
expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "second.txt"), "utf8"))).toBe("second")
yield* Deferred.succeed(releaseFormatting, undefined)
expect((yield* Fiber.join(first)).status).toBe("completed")
})
},
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
})