test(core): simplify mutation lock coverage
This commit is contained in:
@@ -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]()),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user