@markjaquith/agency 3.2.8 → 3.2.10
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.
- package/package.json +1 -1
- package/src/services/ArchiveService.ts +80 -86
- package/src/services/FileSystemService.test.ts +81 -0
- package/src/services/FileSystemService.ts +29 -7
- package/src/services/GraphService.test.ts +11 -0
- package/src/services/GraphService.ts +1 -1
- package/src/services/LifecycleTransaction.test.ts +50 -12
- package/src/services/LifecycleTransaction.ts +152 -133
- package/src/services/PhaseService.ts +7 -6
- package/src/services/RepositoryService.ts +150 -115
- package/src/services/ReviewService.ts +30 -35
- package/src/services/TaskService.ts +23 -21
- package/src/services/WorkbaseService.test.ts +15 -0
- package/src/services/WorkbaseService.ts +10 -4
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema, TreeFormatter } from "@effect/schema"
|
|
2
|
-
import { Data, Effect, Either
|
|
2
|
+
import { Data, Effect, Either } from "effect"
|
|
3
3
|
import { lstat, mkdir, open, rename, rm } from "node:fs/promises"
|
|
4
4
|
import { dirname, join, relative, resolve, sep } from "node:path"
|
|
5
5
|
import { EpicService, type EpicRecord } from "./EpicService"
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
directoryMoveStep,
|
|
41
41
|
documentWriteStep,
|
|
42
42
|
runLifecycleTransaction,
|
|
43
|
+
transactionEffect,
|
|
43
44
|
type TransactionStep,
|
|
44
45
|
} from "./LifecycleTransaction"
|
|
45
46
|
import { withWorktreeLocks } from "./WorktreeLock"
|
|
@@ -358,21 +359,6 @@ const applyMutation = (moves: readonly Move[], writes: readonly Write[]) =>
|
|
|
358
359
|
}),
|
|
359
360
|
})
|
|
360
361
|
|
|
361
|
-
const WorktreeLayer = Layer.mergeAll(
|
|
362
|
-
FileSystemService.Default,
|
|
363
|
-
WorkbaseService.Default,
|
|
364
|
-
GitVersionControlService.Default,
|
|
365
|
-
VersionControlService.Default,
|
|
366
|
-
TaskService.Default,
|
|
367
|
-
PhaseService.Default,
|
|
368
|
-
WorktreeService.Default,
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
const runWorktreeEffect = <A, E>(effect: Effect.Effect<A, E, any>) =>
|
|
372
|
-
Effect.runPromise(
|
|
373
|
-
effect.pipe(Effect.provide(WorktreeLayer)) as Effect.Effect<A, E, never>,
|
|
374
|
-
)
|
|
375
|
-
|
|
376
362
|
const runGit = async (args: readonly string[]) => {
|
|
377
363
|
const process = Bun.spawn([...args], { stdout: "pipe", stderr: "pipe" })
|
|
378
364
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
@@ -791,25 +777,26 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
|
|
|
791
777
|
})
|
|
792
778
|
if (!options.dryRun) {
|
|
793
779
|
const snapshots: WorktreeRemovalSnapshot[] = []
|
|
794
|
-
const steps: TransactionStep[] = [
|
|
780
|
+
const steps: TransactionStep<any>[] = [
|
|
795
781
|
documentWriteStep(root, writes),
|
|
796
782
|
{
|
|
797
783
|
label: `remove worktrees for epic ${id}`,
|
|
798
|
-
apply:
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
784
|
+
apply: Effect.gen(function* () {
|
|
785
|
+
for (const unit of executionUnits)
|
|
786
|
+
yield* worktrees.remove(unit.taskId, unit.phaseId, root, {
|
|
787
|
+
snapshots,
|
|
788
|
+
lockHeld: true,
|
|
789
|
+
})
|
|
790
|
+
}).pipe(
|
|
791
|
+
Effect.catchAllCause((cause) =>
|
|
792
|
+
transactionEffect(() =>
|
|
793
|
+
restoreWorktreeSnapshots(snapshots),
|
|
794
|
+
).pipe(Effect.zipRight(Effect.failCause(cause))),
|
|
795
|
+
),
|
|
796
|
+
),
|
|
797
|
+
rollback: transactionEffect(() =>
|
|
798
|
+
restoreWorktreeSnapshots(snapshots),
|
|
799
|
+
),
|
|
813
800
|
manualRecovery: `Run agency work prepare for each execution unit in epic '${id}'`,
|
|
814
801
|
},
|
|
815
802
|
]
|
|
@@ -1074,34 +1061,33 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
|
|
|
1074
1061
|
)
|
|
1075
1062
|
if (!options.dryRun && selected.length > 0) {
|
|
1076
1063
|
const snapshots: WorktreeRemovalSnapshot[] = []
|
|
1077
|
-
const steps: TransactionStep[] = [
|
|
1064
|
+
const steps: TransactionStep<any>[] = [
|
|
1078
1065
|
documentWriteStep(root, writes),
|
|
1079
1066
|
{
|
|
1080
1067
|
label: "remove worktrees for task archive cohort",
|
|
1081
|
-
apply:
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
(phase) => phase.id === unit.phaseId,
|
|
1094
|
-
)
|
|
1095
|
-
: undefined,
|
|
1096
|
-
}),
|
|
1097
|
-
)
|
|
1098
|
-
}
|
|
1099
|
-
} catch (cause) {
|
|
1100
|
-
await restoreWorktreeSnapshots(snapshots)
|
|
1101
|
-
throw cause
|
|
1068
|
+
apply: Effect.gen(function* () {
|
|
1069
|
+
for (const unit of executionUnits) {
|
|
1070
|
+
yield* worktrees.remove(unit.taskId, unit.phaseId, root, {
|
|
1071
|
+
snapshots,
|
|
1072
|
+
lockHeld: true,
|
|
1073
|
+
task: contexts.get(unit.taskId)!.task,
|
|
1074
|
+
phase: unit.phaseId
|
|
1075
|
+
? contexts
|
|
1076
|
+
.get(unit.taskId)!
|
|
1077
|
+
.phases.find((phase) => phase.id === unit.phaseId)
|
|
1078
|
+
: undefined,
|
|
1079
|
+
})
|
|
1102
1080
|
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1081
|
+
}).pipe(
|
|
1082
|
+
Effect.catchAllCause((cause) =>
|
|
1083
|
+
transactionEffect(() =>
|
|
1084
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1085
|
+
).pipe(Effect.zipRight(Effect.failCause(cause))),
|
|
1086
|
+
),
|
|
1087
|
+
),
|
|
1088
|
+
rollback: transactionEffect(() =>
|
|
1089
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1090
|
+
),
|
|
1105
1091
|
manualRecovery:
|
|
1106
1092
|
"Run agency work prepare for each archived execution unit",
|
|
1107
1093
|
},
|
|
@@ -1281,30 +1267,31 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
|
|
|
1281
1267
|
})
|
|
1282
1268
|
if (!options.dryRun) {
|
|
1283
1269
|
const snapshots: WorktreeRemovalSnapshot[] = []
|
|
1284
|
-
const steps: TransactionStep[] = [
|
|
1270
|
+
const steps: TransactionStep<any>[] = [
|
|
1285
1271
|
documentWriteStep(root, writes),
|
|
1286
1272
|
{
|
|
1287
1273
|
label: `remove worktrees for task ${id}`,
|
|
1288
|
-
apply:
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1274
|
+
apply: Effect.gen(function* () {
|
|
1275
|
+
for (const unit of executionUnits)
|
|
1276
|
+
yield* worktrees.remove(
|
|
1277
|
+
unit.taskId,
|
|
1278
|
+
executionPhaseId(unit),
|
|
1279
|
+
root,
|
|
1280
|
+
{
|
|
1281
|
+
snapshots,
|
|
1282
|
+
lockHeld: true,
|
|
1283
|
+
},
|
|
1284
|
+
)
|
|
1285
|
+
}).pipe(
|
|
1286
|
+
Effect.catchAllCause((cause) =>
|
|
1287
|
+
transactionEffect(() =>
|
|
1288
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1289
|
+
).pipe(Effect.zipRight(Effect.failCause(cause))),
|
|
1290
|
+
),
|
|
1291
|
+
),
|
|
1292
|
+
rollback: transactionEffect(() =>
|
|
1293
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1294
|
+
),
|
|
1308
1295
|
manualRecovery: `Run agency work prepare for task '${id}'`,
|
|
1309
1296
|
},
|
|
1310
1297
|
directoryMoveStep(root, dirname(task.path), destination),
|
|
@@ -1426,15 +1413,22 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
|
|
|
1426
1413
|
documentWriteStep(root, writes),
|
|
1427
1414
|
{
|
|
1428
1415
|
label: `remove worktrees for phase ${taskId}/${id}`,
|
|
1429
|
-
apply:
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1416
|
+
apply: worktrees
|
|
1417
|
+
.remove(taskId, id, root, {
|
|
1418
|
+
snapshots,
|
|
1419
|
+
lockHeld: true,
|
|
1420
|
+
})
|
|
1421
|
+
.pipe(
|
|
1422
|
+
Effect.asVoid,
|
|
1423
|
+
Effect.catchAllCause((cause) =>
|
|
1424
|
+
transactionEffect(() =>
|
|
1425
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1426
|
+
).pipe(Effect.zipRight(Effect.failCause(cause))),
|
|
1427
|
+
),
|
|
1428
|
+
),
|
|
1429
|
+
rollback: transactionEffect(() =>
|
|
1430
|
+
restoreWorktreeSnapshots(snapshots),
|
|
1431
|
+
),
|
|
1438
1432
|
manualRecovery: `Run agency work prepare for phase '${taskId}/${id}'`,
|
|
1439
1433
|
},
|
|
1440
1434
|
directoryMoveStep(root, dirname(phase.path), destination),
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { mkdir } from "node:fs/promises"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { cleanupTempDir, createTempDir } from "../test-utils"
|
|
6
|
+
import { FileSystemService } from "./FileSystemService"
|
|
7
|
+
|
|
8
|
+
const runFileSystem = <A, E>(effect: Effect.Effect<A, E, FileSystemService>) =>
|
|
9
|
+
Effect.runPromise(effect.pipe(Effect.provide(FileSystemService.Default)))
|
|
10
|
+
|
|
11
|
+
describe("FileSystemService", () => {
|
|
12
|
+
const roots: string[] = []
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await Promise.all(roots.splice(0).map(cleanupTempDir))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("distinguishes missing files from other read failures", async () => {
|
|
19
|
+
const root = await createTempDir()
|
|
20
|
+
roots.push(root)
|
|
21
|
+
const directory = join(root, "document")
|
|
22
|
+
await mkdir(directory)
|
|
23
|
+
|
|
24
|
+
const missing = await runFileSystem(
|
|
25
|
+
FileSystemService.pipe(
|
|
26
|
+
Effect.flatMap((service) => service.readFile(join(root, "missing"))),
|
|
27
|
+
Effect.either,
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
expect(missing).toMatchObject({
|
|
31
|
+
_tag: "Left",
|
|
32
|
+
left: { _tag: "FileNotFoundError" },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const unreadable = await runFileSystem(
|
|
36
|
+
FileSystemService.pipe(
|
|
37
|
+
Effect.flatMap((service) => service.readFile(directory)),
|
|
38
|
+
Effect.either,
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
expect(unreadable).toMatchObject({
|
|
42
|
+
_tag: "Left",
|
|
43
|
+
left: {
|
|
44
|
+
_tag: "FileSystemError",
|
|
45
|
+
message: `Failed to read file: ${directory}`,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("only treats a missing symlink path as absent", async () => {
|
|
51
|
+
const root = await createTempDir()
|
|
52
|
+
roots.push(root)
|
|
53
|
+
const regularFile = join(root, "regular")
|
|
54
|
+
await Bun.write(regularFile, "content")
|
|
55
|
+
|
|
56
|
+
await expect(
|
|
57
|
+
runFileSystem(
|
|
58
|
+
FileSystemService.pipe(
|
|
59
|
+
Effect.flatMap((service) =>
|
|
60
|
+
service.readSymlinkTarget(join(root, "missing")),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
).resolves.toBeNull()
|
|
65
|
+
const unreadable = await runFileSystem(
|
|
66
|
+
FileSystemService.pipe(
|
|
67
|
+
Effect.flatMap((service) =>
|
|
68
|
+
service.readSymlinkTarget(join(regularFile, "child")),
|
|
69
|
+
),
|
|
70
|
+
Effect.either,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
expect(unreadable).toMatchObject({
|
|
74
|
+
_tag: "Left",
|
|
75
|
+
left: {
|
|
76
|
+
_tag: "FileSystemError",
|
|
77
|
+
message: `Failed to read symlink target: ${join(regularFile, "child")}`,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -23,6 +23,12 @@ class FileNotFoundError extends Data.TaggedError("FileNotFoundError")<{
|
|
|
23
23
|
path: string
|
|
24
24
|
}> {}
|
|
25
25
|
|
|
26
|
+
const isFileNotFound = (error: unknown) =>
|
|
27
|
+
typeof error === "object" &&
|
|
28
|
+
error !== null &&
|
|
29
|
+
"code" in error &&
|
|
30
|
+
error.code === "ENOENT"
|
|
31
|
+
|
|
26
32
|
// FileSystem Service using Effect.Service pattern
|
|
27
33
|
export class FileSystemService extends Effect.Service<FileSystemService>()(
|
|
28
34
|
"FileSystemService",
|
|
@@ -88,7 +94,13 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
|
|
|
88
94
|
readFile: (path: string) =>
|
|
89
95
|
Effect.tryPromise({
|
|
90
96
|
try: () => Bun.file(path).text(),
|
|
91
|
-
catch: () =>
|
|
97
|
+
catch: (error) =>
|
|
98
|
+
isFileNotFound(error)
|
|
99
|
+
? new FileNotFoundError({ path })
|
|
100
|
+
: new FileSystemError({
|
|
101
|
+
message: `Failed to read file: ${path}`,
|
|
102
|
+
cause: error,
|
|
103
|
+
}),
|
|
92
104
|
}),
|
|
93
105
|
|
|
94
106
|
inspectFile: (path: string) =>
|
|
@@ -184,10 +196,12 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
|
|
|
184
196
|
}))
|
|
185
197
|
},
|
|
186
198
|
catch: (error) =>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
199
|
+
isFileNotFound(error)
|
|
200
|
+
? new FileNotFoundError({ path })
|
|
201
|
+
: new FileSystemError({
|
|
202
|
+
message: `Failed to read directory: ${path}`,
|
|
203
|
+
cause: error,
|
|
204
|
+
}),
|
|
191
205
|
}),
|
|
192
206
|
|
|
193
207
|
deleteDirectory: (path: string) =>
|
|
@@ -301,8 +315,16 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
|
|
|
301
315
|
}
|
|
302
316
|
return await readlink(path)
|
|
303
317
|
},
|
|
304
|
-
catch: () =>
|
|
305
|
-
|
|
318
|
+
catch: (error) =>
|
|
319
|
+
isFileNotFound(error)
|
|
320
|
+
? new FileNotFoundError({ path })
|
|
321
|
+
: new FileSystemError({
|
|
322
|
+
message: `Failed to read symlink target: ${path}`,
|
|
323
|
+
cause: error,
|
|
324
|
+
}),
|
|
325
|
+
}).pipe(
|
|
326
|
+
Effect.catchTag("FileNotFoundError", () => Effect.succeed(null)),
|
|
327
|
+
),
|
|
306
328
|
}),
|
|
307
329
|
},
|
|
308
330
|
) {}
|
|
@@ -230,6 +230,17 @@ describe("GraphService", () => {
|
|
|
230
230
|
expect(calls).toBe(0)
|
|
231
231
|
})
|
|
232
232
|
|
|
233
|
+
test("propagates directory discovery failures", async () => {
|
|
234
|
+
const root = await createTempDir()
|
|
235
|
+
roots.push(root)
|
|
236
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
237
|
+
await write(root, "tasks", "not a directory")
|
|
238
|
+
|
|
239
|
+
await expect(getGraph(root)).rejects.toThrow(
|
|
240
|
+
`Failed to read directory: ${join(root, "tasks")}`,
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
|
|
233
244
|
test("never reports active or terminal execution units as ready", async () => {
|
|
234
245
|
const root = await createWorkbase()
|
|
235
246
|
roots.push(root)
|
|
@@ -158,7 +158,7 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
158
158
|
.map((entry) => entry.name)
|
|
159
159
|
.sort(),
|
|
160
160
|
),
|
|
161
|
-
Effect.
|
|
161
|
+
Effect.catchTag("FileNotFoundError", () => Effect.succeed([])),
|
|
162
162
|
)
|
|
163
163
|
|
|
164
164
|
const readDocument = <S extends Schema.Schema.AnyNoContext>(
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { Effect } from "effect"
|
|
2
|
+
import { Deferred, Effect, Fiber } from "effect"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { cleanupTempDir, createTempDir } from "../test-utils"
|
|
5
5
|
import {
|
|
6
6
|
documentWriteStep,
|
|
7
7
|
runLifecycleTransaction,
|
|
8
|
+
transactionEffect,
|
|
8
9
|
} from "./LifecycleTransaction"
|
|
9
10
|
|
|
10
11
|
describe("lifecycle transactions", () => {
|
|
@@ -25,14 +26,16 @@ describe("lifecycle transactions", () => {
|
|
|
25
26
|
steps: [
|
|
26
27
|
{
|
|
27
28
|
label: "write marker",
|
|
28
|
-
apply: (
|
|
29
|
+
apply: transactionEffect(async () => {
|
|
30
|
+
await Bun.write(marker, "applied")
|
|
31
|
+
}),
|
|
29
32
|
},
|
|
30
33
|
{
|
|
31
34
|
label: "reject plan",
|
|
32
|
-
preflight:
|
|
35
|
+
preflight: Effect.sync(() => {
|
|
33
36
|
throw new Error("preflight rejected")
|
|
34
|
-
},
|
|
35
|
-
apply:
|
|
37
|
+
}),
|
|
38
|
+
apply: Effect.void,
|
|
36
39
|
},
|
|
37
40
|
],
|
|
38
41
|
}),
|
|
@@ -57,9 +60,9 @@ describe("lifecycle transactions", () => {
|
|
|
57
60
|
]),
|
|
58
61
|
{
|
|
59
62
|
label: "fail after documents",
|
|
60
|
-
apply:
|
|
63
|
+
apply: Effect.sync(() => {
|
|
61
64
|
throw new Error("injected failure")
|
|
62
|
-
},
|
|
65
|
+
}),
|
|
63
66
|
},
|
|
64
67
|
],
|
|
65
68
|
}).pipe(Effect.either),
|
|
@@ -83,17 +86,17 @@ describe("lifecycle transactions", () => {
|
|
|
83
86
|
steps: [
|
|
84
87
|
{
|
|
85
88
|
label: "external mutation",
|
|
86
|
-
apply:
|
|
87
|
-
rollback:
|
|
89
|
+
apply: Effect.void,
|
|
90
|
+
rollback: Effect.sync(() => {
|
|
88
91
|
throw new Error("rollback failed")
|
|
89
|
-
},
|
|
92
|
+
}),
|
|
90
93
|
manualRecovery: "undo external mutation",
|
|
91
94
|
},
|
|
92
95
|
{
|
|
93
96
|
label: "injected failure",
|
|
94
|
-
apply:
|
|
97
|
+
apply: Effect.sync(() => {
|
|
95
98
|
throw new Error("apply failed")
|
|
96
|
-
},
|
|
99
|
+
}),
|
|
97
100
|
},
|
|
98
101
|
],
|
|
99
102
|
}).pipe(Effect.either),
|
|
@@ -104,4 +107,39 @@ describe("lifecycle transactions", () => {
|
|
|
104
107
|
expect(failure.rolledBack).toEqual([])
|
|
105
108
|
expect(failure.manualRecovery).toEqual(["undo external mutation"])
|
|
106
109
|
})
|
|
110
|
+
|
|
111
|
+
test("waits for rollback and lock release when interrupted", async () => {
|
|
112
|
+
const events: string[] = []
|
|
113
|
+
const blocked = await Effect.runPromise(Deferred.make<void>())
|
|
114
|
+
const fiber = Effect.runFork(
|
|
115
|
+
runLifecycleTransaction({
|
|
116
|
+
root,
|
|
117
|
+
steps: [
|
|
118
|
+
{
|
|
119
|
+
label: "external mutation",
|
|
120
|
+
apply: Effect.sync(() => events.push("applied")),
|
|
121
|
+
rollback: transactionEffect(async () => {
|
|
122
|
+
events.push("rollback started")
|
|
123
|
+
await Bun.sleep(20)
|
|
124
|
+
events.push("rollback finished")
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
label: "block",
|
|
129
|
+
apply: Deferred.succeed(blocked, undefined).pipe(
|
|
130
|
+
Effect.zipRight(Effect.never),
|
|
131
|
+
),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
await Effect.runPromise(Deferred.await(blocked))
|
|
137
|
+
|
|
138
|
+
await Effect.runPromise(Fiber.interrupt(fiber))
|
|
139
|
+
|
|
140
|
+
expect(events).toEqual(["applied", "rollback started", "rollback finished"])
|
|
141
|
+
expect(
|
|
142
|
+
await Bun.file(join(root, ".agency-graph-mutation.lock")).exists(),
|
|
143
|
+
).toBe(false)
|
|
144
|
+
})
|
|
107
145
|
})
|