@markjaquith/agency 3.2.13 → 3.2.15
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 +7 -2
- package/src/workbase/schemas.ts +12 -0
- package/fixtures/protocol/orchestration-recipes.json +0 -53
- package/pi-extensions/agency.test.ts +0 -117
- package/src/check-commits.test.ts +0 -58
- package/src/cli-parser.test.ts +0 -945
- package/src/cli.test.ts +0 -1889
- package/src/commands/act.test.ts +0 -418
- package/src/commands/archive.test.ts +0 -236
- package/src/commands/context.test.ts +0 -665
- package/src/commands/description.test.ts +0 -103
- package/src/commands/doctor.test.ts +0 -185
- package/src/commands/epic.test.ts +0 -196
- package/src/commands/init.test.ts +0 -117
- package/src/commands/integration.test.ts +0 -165
- package/src/commands/pr.test.ts +0 -248
- package/src/commands/push.test.ts +0 -56
- package/src/commands/read-only.test.ts +0 -161
- package/src/commands/repo.test.ts +0 -199
- package/src/commands/restore.test.ts +0 -106
- package/src/commands/status.test.ts +0 -113
- package/src/commands/sync.test.ts +0 -200
- package/src/commands/task-phase.test.ts +0 -410
- package/src/commands/task.test.ts +0 -281
- package/src/commands/validate.test.ts +0 -186
- package/src/commands/work.test.ts +0 -1375
- package/src/commands/workbase.test.ts +0 -170
- package/src/graph-schema.test.ts +0 -124
- package/src/protocol.test.ts +0 -163
- package/src/readiness.test.ts +0 -105
- package/src/services/ArchiveBulkService.test.ts +0 -384
- package/src/services/ArchiveService.test.ts +0 -1092
- package/src/services/EpicService.test.ts +0 -74
- package/src/services/FileSystemService.test.ts +0 -81
- package/src/services/GraphMutationService.test.ts +0 -486
- package/src/services/GraphService.test.ts +0 -494
- package/src/services/IntegrationService.test.ts +0 -1091
- package/src/services/LifecycleTransaction.test.ts +0 -145
- package/src/services/PullRequestService.test.ts +0 -716
- package/src/services/PushService.test.ts +0 -408
- package/src/services/ReadinessService.test.ts +0 -290
- package/src/services/RepositoryService.test.ts +0 -789
- package/src/services/ReviewService.test.ts +0 -408
- package/src/services/SyncService.test.ts +0 -1377
- package/src/services/TaskPhaseService.test.ts +0 -852
- package/src/services/VersionControlService.test.ts +0 -62
- package/src/services/WorkbaseService.test.ts +0 -910
- package/src/services/WorktreeLock.test.ts +0 -205
- package/src/services/WorktreePerformance.test.ts +0 -71
- package/src/services/WorktreeService.test.ts +0 -2292
- package/src/test-utils.ts +0 -110
- package/src/usage-log.test.ts +0 -188
- package/src/utils/chooser.test.ts +0 -192
- package/src/utils/effect.test.ts +0 -30
- package/src/utils/interactive.pty.test.ts +0 -128
- package/src/utils/interactive.test.tsx +0 -860
- package/src/utils/process.test.ts +0 -132
- package/src/utils/progress.test.ts +0 -37
- package/src/work-view.test.ts +0 -151
- package/src/workbase/agent-command.test.ts +0 -128
- package/src/workbase/checkout-command.test.ts +0 -62
- package/src/workbase/delivery-command.test.ts +0 -180
- package/src/workbase/dependency-graph.test.ts +0 -50
- package/src/workbase/execution-contract.test.ts +0 -161
- package/src/workbase/frontmatter.test.ts +0 -67
- package/src/workbase/repository-reference.test.ts +0 -25
- package/src/workbase/schemas.test.ts +0 -543
- package/src/workbase/work-target.test.ts +0 -108
- package/src/workbase/worktree-command.test.ts +0 -61
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdir } from "node:fs/promises"
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
5
|
-
import { Effect } from "effect"
|
|
6
|
-
import { EpicService } from "./EpicService"
|
|
7
|
-
|
|
8
|
-
describe("EpicService", () => {
|
|
9
|
-
let root: string
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
root = await createTempDir()
|
|
13
|
-
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
14
|
-
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
afterEach(async () => {
|
|
18
|
-
await cleanupTempDir(root)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test("creates, lists, and shows epics", async () => {
|
|
22
|
-
const created = await runTestEffect(
|
|
23
|
-
EpicService.pipe(
|
|
24
|
-
Effect.flatMap((service) =>
|
|
25
|
-
service.create(
|
|
26
|
-
"workspace-orchestration",
|
|
27
|
-
"https://example.com/tickets/epic",
|
|
28
|
-
[{ repo: "agency", ref: "main" }],
|
|
29
|
-
root,
|
|
30
|
-
),
|
|
31
|
-
),
|
|
32
|
-
),
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
expect(created.content).toContain("ticketUrl:")
|
|
36
|
-
expect(created.content).toContain("tasks: []")
|
|
37
|
-
expect(created.content).toContain(
|
|
38
|
-
"# Workspace Orchestration\n\n## Outcome\n\nDescribe the epic outcome.\n\n## Plan\n\nDescribe the current approach.\n\n## Important Decisions\n\nRecord consequential decisions and their rationale.",
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
const records = await runTestEffect(
|
|
42
|
-
EpicService.pipe(Effect.flatMap((service) => service.list(root))),
|
|
43
|
-
)
|
|
44
|
-
expect(records.map((record) => record.id)).toEqual([
|
|
45
|
-
"workspace-orchestration",
|
|
46
|
-
])
|
|
47
|
-
|
|
48
|
-
const shown = await runTestEffect(
|
|
49
|
-
EpicService.pipe(
|
|
50
|
-
Effect.flatMap((service) =>
|
|
51
|
-
service.show("workspace-orchestration", root),
|
|
52
|
-
),
|
|
53
|
-
),
|
|
54
|
-
)
|
|
55
|
-
expect(shown.path).toBe(join(root, "epics/workspace-orchestration/EPIC.md"))
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test("rejects unknown repository aliases", async () => {
|
|
59
|
-
await expect(
|
|
60
|
-
runTestEffect(
|
|
61
|
-
EpicService.pipe(
|
|
62
|
-
Effect.flatMap((service) =>
|
|
63
|
-
service.create(
|
|
64
|
-
"example",
|
|
65
|
-
"https://example.com/tickets/epic",
|
|
66
|
-
[{ repo: "missing", ref: "main" }],
|
|
67
|
-
root,
|
|
68
|
-
),
|
|
69
|
-
),
|
|
70
|
-
),
|
|
71
|
-
),
|
|
72
|
-
).rejects.toThrow("Unknown repository alias 'missing'")
|
|
73
|
-
})
|
|
74
|
-
})
|
|
@@ -1,81 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,486 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdir } from "node:fs/promises"
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
import { Effect } from "effect"
|
|
5
|
-
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
|
-
import { EpicService } from "./EpicService"
|
|
7
|
-
import { GraphMutationService } from "./GraphMutationService"
|
|
8
|
-
import { PhaseService } from "./PhaseService"
|
|
9
|
-
import { TaskService } from "./TaskService"
|
|
10
|
-
import {
|
|
11
|
-
formatMarkdownDocument,
|
|
12
|
-
parseFrontmatter,
|
|
13
|
-
} from "../workbase/frontmatter"
|
|
14
|
-
|
|
15
|
-
describe("GraphMutationService", () => {
|
|
16
|
-
let root: string
|
|
17
|
-
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
root = await createTempDir()
|
|
20
|
-
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
21
|
-
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
22
|
-
await mkdir(join(root, "repos/docs"), { recursive: true })
|
|
23
|
-
await runTestEffect(
|
|
24
|
-
Effect.gen(function* () {
|
|
25
|
-
const epics = yield* EpicService
|
|
26
|
-
const tasks = yield* TaskService
|
|
27
|
-
const phases = yield* PhaseService
|
|
28
|
-
yield* epics.create(
|
|
29
|
-
"first-epic",
|
|
30
|
-
"https://example.com/first",
|
|
31
|
-
[{ repo: "agency", ref: "main" }],
|
|
32
|
-
root,
|
|
33
|
-
)
|
|
34
|
-
yield* epics.create(
|
|
35
|
-
"second-epic",
|
|
36
|
-
"https://example.com/second",
|
|
37
|
-
[{ repo: "agency", ref: "main" }],
|
|
38
|
-
root,
|
|
39
|
-
)
|
|
40
|
-
for (const id of ["alpha", "beta", "gamma"]) {
|
|
41
|
-
yield* tasks.create(
|
|
42
|
-
{
|
|
43
|
-
id,
|
|
44
|
-
ticketUrl: null,
|
|
45
|
-
epic: "first-epic",
|
|
46
|
-
repo: "agency",
|
|
47
|
-
branch: `task/${id}`,
|
|
48
|
-
base: "main",
|
|
49
|
-
},
|
|
50
|
-
root,
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
yield* tasks.create(
|
|
54
|
-
{
|
|
55
|
-
id: "multi",
|
|
56
|
-
ticketUrl: "https://example.com/multi",
|
|
57
|
-
multiPhase: true,
|
|
58
|
-
},
|
|
59
|
-
root,
|
|
60
|
-
)
|
|
61
|
-
yield* phases.create(
|
|
62
|
-
{
|
|
63
|
-
taskId: "multi",
|
|
64
|
-
id: "build",
|
|
65
|
-
repo: "agency",
|
|
66
|
-
branch: "task/multi-build",
|
|
67
|
-
base: "main",
|
|
68
|
-
},
|
|
69
|
-
root,
|
|
70
|
-
)
|
|
71
|
-
yield* phases.create(
|
|
72
|
-
{
|
|
73
|
-
taskId: "multi",
|
|
74
|
-
id: "ship",
|
|
75
|
-
repo: "agency",
|
|
76
|
-
branch: "task/multi-ship",
|
|
77
|
-
base: "main",
|
|
78
|
-
},
|
|
79
|
-
root,
|
|
80
|
-
)
|
|
81
|
-
}),
|
|
82
|
-
)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
afterEach(async () => cleanupTempDir(root))
|
|
86
|
-
|
|
87
|
-
test("updates descriptions, tickets, repositories, and execution metadata", async () => {
|
|
88
|
-
const output = await runTestEffect(
|
|
89
|
-
Effect.gen(function* () {
|
|
90
|
-
const mutations = yield* GraphMutationService
|
|
91
|
-
yield* mutations.updateEpic(
|
|
92
|
-
"first-epic",
|
|
93
|
-
{
|
|
94
|
-
description: "Updated epic",
|
|
95
|
-
ticketUrl: "https://example.com/revised",
|
|
96
|
-
repos: [{ repo: "docs", ref: "trunk" }],
|
|
97
|
-
},
|
|
98
|
-
root,
|
|
99
|
-
)
|
|
100
|
-
yield* mutations.updateTask(
|
|
101
|
-
"alpha",
|
|
102
|
-
{
|
|
103
|
-
description: "Updated task",
|
|
104
|
-
ticketUrl: "https://example.com/alpha",
|
|
105
|
-
repos: [{ repo: "docs", ref: "main" }],
|
|
106
|
-
branch: "feature/alpha",
|
|
107
|
-
base: "develop",
|
|
108
|
-
pr: "https://github.com/example/agency/pull/12",
|
|
109
|
-
},
|
|
110
|
-
root,
|
|
111
|
-
)
|
|
112
|
-
return yield* mutations.updatePhase(
|
|
113
|
-
"multi",
|
|
114
|
-
"build",
|
|
115
|
-
{
|
|
116
|
-
description: "Updated phase",
|
|
117
|
-
repo: "docs",
|
|
118
|
-
branch: "feature/build",
|
|
119
|
-
base: "develop",
|
|
120
|
-
},
|
|
121
|
-
root,
|
|
122
|
-
)
|
|
123
|
-
}),
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
expect(output).toMatchObject({
|
|
127
|
-
operation: "phase.update",
|
|
128
|
-
changed: true,
|
|
129
|
-
validation: { valid: true },
|
|
130
|
-
})
|
|
131
|
-
await runTestEffect(
|
|
132
|
-
Effect.gen(function* () {
|
|
133
|
-
const epics = yield* EpicService
|
|
134
|
-
const tasks = yield* TaskService
|
|
135
|
-
const phases = yield* PhaseService
|
|
136
|
-
expect((yield* epics.show("first-epic", root)).data).toMatchObject({
|
|
137
|
-
description: "Updated epic",
|
|
138
|
-
ticketUrl: "https://example.com/revised",
|
|
139
|
-
repos: [{ repo: "docs", ref: "trunk" }],
|
|
140
|
-
})
|
|
141
|
-
expect((yield* tasks.show("alpha", root)).data).toMatchObject({
|
|
142
|
-
description: "Updated task",
|
|
143
|
-
ticketUrl: "https://example.com/alpha",
|
|
144
|
-
repos: [{ repo: "docs", ref: "main" }],
|
|
145
|
-
branch: "feature/alpha",
|
|
146
|
-
base: "develop",
|
|
147
|
-
pr: "https://github.com/example/agency/pull/12",
|
|
148
|
-
})
|
|
149
|
-
expect((yield* phases.show("multi", "build", root)).data).toMatchObject(
|
|
150
|
-
{
|
|
151
|
-
description: "Updated phase",
|
|
152
|
-
repo: "docs",
|
|
153
|
-
branch: "feature/build",
|
|
154
|
-
base: "develop",
|
|
155
|
-
},
|
|
156
|
-
)
|
|
157
|
-
}),
|
|
158
|
-
)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
test("sets and clears pull request metadata with materialized code", async () => {
|
|
162
|
-
await mkdir(join(root, "tasks/alpha/code/agency"), { recursive: true })
|
|
163
|
-
await mkdir(join(root, "tasks/multi/phases/build/code/agency"), {
|
|
164
|
-
recursive: true,
|
|
165
|
-
})
|
|
166
|
-
const taskPr = "https://github.com/example/agency/pull/12"
|
|
167
|
-
const phasePr = "https://github.com/example/agency/pull/13"
|
|
168
|
-
|
|
169
|
-
await runTestEffect(
|
|
170
|
-
Effect.gen(function* () {
|
|
171
|
-
const mutations = yield* GraphMutationService
|
|
172
|
-
yield* mutations.updateTask("alpha", { pr: taskPr }, root)
|
|
173
|
-
yield* mutations.updatePhase("multi", "build", { pr: phasePr }, root)
|
|
174
|
-
const tasks = yield* TaskService
|
|
175
|
-
const phases = yield* PhaseService
|
|
176
|
-
expect((yield* tasks.show("alpha", root)).data).toMatchObject({
|
|
177
|
-
pr: taskPr,
|
|
178
|
-
})
|
|
179
|
-
expect((yield* phases.show("multi", "build", root)).data.pr).toBe(
|
|
180
|
-
phasePr,
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
yield* mutations.updateTask("alpha", { pr: null }, root)
|
|
184
|
-
yield* mutations.updatePhase("multi", "build", { pr: null }, root)
|
|
185
|
-
expect((yield* tasks.show("alpha", root)).data).toMatchObject({
|
|
186
|
-
pr: null,
|
|
187
|
-
})
|
|
188
|
-
expect((yield* phases.show("multi", "build", root)).data.pr).toBeNull()
|
|
189
|
-
}),
|
|
190
|
-
)
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
test("rejects checkout topology updates with materialized code", async () => {
|
|
194
|
-
await mkdir(join(root, "tasks/alpha/code/agency"), { recursive: true })
|
|
195
|
-
await mkdir(join(root, "tasks/multi/phases/build/code/agency"), {
|
|
196
|
-
recursive: true,
|
|
197
|
-
})
|
|
198
|
-
const topologyUpdates = [
|
|
199
|
-
{ repo: "docs" },
|
|
200
|
-
{ repos: [{ repo: "docs", ref: "main" }] },
|
|
201
|
-
{ branch: "feature/materialized" },
|
|
202
|
-
{ base: "develop" },
|
|
203
|
-
]
|
|
204
|
-
|
|
205
|
-
for (const updates of topologyUpdates) {
|
|
206
|
-
await expect(
|
|
207
|
-
runTestEffect(
|
|
208
|
-
Effect.gen(function* () {
|
|
209
|
-
return yield* (yield* GraphMutationService).updateTask(
|
|
210
|
-
"alpha",
|
|
211
|
-
updates,
|
|
212
|
-
root,
|
|
213
|
-
)
|
|
214
|
-
}),
|
|
215
|
-
),
|
|
216
|
-
).rejects.toThrow("materialized code")
|
|
217
|
-
await expect(
|
|
218
|
-
runTestEffect(
|
|
219
|
-
Effect.gen(function* () {
|
|
220
|
-
return yield* (yield* GraphMutationService).updatePhase(
|
|
221
|
-
"multi",
|
|
222
|
-
"build",
|
|
223
|
-
updates,
|
|
224
|
-
root,
|
|
225
|
-
)
|
|
226
|
-
}),
|
|
227
|
-
),
|
|
228
|
-
).rejects.toThrow("materialized code")
|
|
229
|
-
}
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
test("preserves dependency order and rejects cycles", async () => {
|
|
233
|
-
await runTestEffect(
|
|
234
|
-
Effect.gen(function* () {
|
|
235
|
-
const mutations = yield* GraphMutationService
|
|
236
|
-
yield* mutations.mutateTaskDependency("add", "beta", "alpha", root)
|
|
237
|
-
yield* mutations.mutateTaskDependency("add", "beta", "gamma", root)
|
|
238
|
-
yield* mutations.mutateTaskDependency("remove", "beta", "alpha", root)
|
|
239
|
-
yield* mutations.mutatePhaseDependency(
|
|
240
|
-
"add",
|
|
241
|
-
"multi",
|
|
242
|
-
"ship",
|
|
243
|
-
"build",
|
|
244
|
-
root,
|
|
245
|
-
)
|
|
246
|
-
}),
|
|
247
|
-
)
|
|
248
|
-
await expect(
|
|
249
|
-
runTestEffect(
|
|
250
|
-
Effect.gen(function* () {
|
|
251
|
-
return yield* (yield* GraphMutationService).mutateTaskDependency(
|
|
252
|
-
"add",
|
|
253
|
-
"gamma",
|
|
254
|
-
"beta",
|
|
255
|
-
root,
|
|
256
|
-
)
|
|
257
|
-
}),
|
|
258
|
-
),
|
|
259
|
-
).rejects.toThrow("dependency cycle")
|
|
260
|
-
|
|
261
|
-
await expect(
|
|
262
|
-
runTestEffect(
|
|
263
|
-
Effect.gen(function* () {
|
|
264
|
-
return yield* (yield* GraphMutationService).mutateTaskDependency(
|
|
265
|
-
"add",
|
|
266
|
-
"beta",
|
|
267
|
-
"gamma",
|
|
268
|
-
root,
|
|
269
|
-
)
|
|
270
|
-
}),
|
|
271
|
-
),
|
|
272
|
-
).rejects.toThrow("already depends")
|
|
273
|
-
|
|
274
|
-
await runTestEffect(
|
|
275
|
-
Effect.gen(function* () {
|
|
276
|
-
const epics = yield* EpicService
|
|
277
|
-
const tasks = yield* TaskService
|
|
278
|
-
const declarations = (yield* epics.show("first-epic", root)).data.tasks
|
|
279
|
-
expect(
|
|
280
|
-
declarations.find((item) => item.id === "beta")?.dependsOn,
|
|
281
|
-
).toEqual(["gamma"])
|
|
282
|
-
expect((yield* tasks.show("multi", root)).data).toMatchObject({
|
|
283
|
-
phases: [{ id: "build" }, { id: "ship", dependsOn: ["build"] }],
|
|
284
|
-
})
|
|
285
|
-
}),
|
|
286
|
-
)
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
test("renames entities and rewrites every structured reference", async () => {
|
|
290
|
-
await runTestEffect(
|
|
291
|
-
Effect.gen(function* () {
|
|
292
|
-
const mutations = yield* GraphMutationService
|
|
293
|
-
yield* mutations.mutateTaskDependency("add", "beta", "alpha", root)
|
|
294
|
-
yield* mutations.mutatePhaseDependency(
|
|
295
|
-
"add",
|
|
296
|
-
"multi",
|
|
297
|
-
"ship",
|
|
298
|
-
"build",
|
|
299
|
-
root,
|
|
300
|
-
)
|
|
301
|
-
yield* mutations.renameTask("alpha", "renamed-alpha", root)
|
|
302
|
-
yield* mutations.renamePhase("multi", "build", "compile", root)
|
|
303
|
-
yield* mutations.renameEpic("first-epic", "renamed-epic", root)
|
|
304
|
-
}),
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
await runTestEffect(
|
|
308
|
-
Effect.gen(function* () {
|
|
309
|
-
const epics = yield* EpicService
|
|
310
|
-
const tasks = yield* TaskService
|
|
311
|
-
const phases = yield* PhaseService
|
|
312
|
-
const epic = yield* epics.show("renamed-epic", root)
|
|
313
|
-
expect(
|
|
314
|
-
epic.data.tasks.find((item) => item.id === "beta")?.dependsOn,
|
|
315
|
-
).toEqual(["renamed-alpha"])
|
|
316
|
-
expect((yield* tasks.show("renamed-alpha", root)).data.epic).toBe(
|
|
317
|
-
"renamed-epic",
|
|
318
|
-
)
|
|
319
|
-
expect((yield* tasks.show("multi", root)).data).toMatchObject({
|
|
320
|
-
phases: [{ id: "compile" }, { id: "ship", dependsOn: ["compile"] }],
|
|
321
|
-
})
|
|
322
|
-
expect((yield* phases.show("multi", "compile", root)).id).toBe(
|
|
323
|
-
"compile",
|
|
324
|
-
)
|
|
325
|
-
}),
|
|
326
|
-
)
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
test("preserves historical handoff provenance across source and destination renames", async () => {
|
|
330
|
-
await runTestEffect(
|
|
331
|
-
Effect.gen(function* () {
|
|
332
|
-
const tasks = yield* TaskService
|
|
333
|
-
yield* tasks.create(
|
|
334
|
-
{
|
|
335
|
-
id: "investigation",
|
|
336
|
-
ticketUrl: null,
|
|
337
|
-
repo: "agency",
|
|
338
|
-
branch: "task/investigation",
|
|
339
|
-
base: "main",
|
|
340
|
-
purpose: "investigation",
|
|
341
|
-
},
|
|
342
|
-
root,
|
|
343
|
-
)
|
|
344
|
-
yield* tasks.handoff(
|
|
345
|
-
{
|
|
346
|
-
sourceTaskId: "investigation",
|
|
347
|
-
id: "implementation",
|
|
348
|
-
ticketUrl: null,
|
|
349
|
-
repo: "docs",
|
|
350
|
-
branch: "task/implementation",
|
|
351
|
-
base: "main",
|
|
352
|
-
},
|
|
353
|
-
root,
|
|
354
|
-
)
|
|
355
|
-
const before = yield* tasks.show("implementation", root)
|
|
356
|
-
const mutations = yield* GraphMutationService
|
|
357
|
-
yield* mutations.renameTask(
|
|
358
|
-
"investigation",
|
|
359
|
-
"investigation-renamed",
|
|
360
|
-
root,
|
|
361
|
-
)
|
|
362
|
-
yield* mutations.renameTask(
|
|
363
|
-
"implementation",
|
|
364
|
-
"implementation-renamed",
|
|
365
|
-
root,
|
|
366
|
-
)
|
|
367
|
-
const after = yield* tasks.show("implementation-renamed", root)
|
|
368
|
-
expect(after.data.handoff).toEqual(before.data.handoff)
|
|
369
|
-
expect(after.data.handoff?.source).toEqual({
|
|
370
|
-
kind: "task",
|
|
371
|
-
taskId: "investigation",
|
|
372
|
-
})
|
|
373
|
-
}),
|
|
374
|
-
)
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
test("moves task membership in both directions", async () => {
|
|
378
|
-
await runTestEffect(
|
|
379
|
-
Effect.gen(function* () {
|
|
380
|
-
yield* (yield* GraphMutationService).moveTask(
|
|
381
|
-
"alpha",
|
|
382
|
-
"second-epic",
|
|
383
|
-
root,
|
|
384
|
-
)
|
|
385
|
-
const epics = yield* EpicService
|
|
386
|
-
const tasks = yield* TaskService
|
|
387
|
-
expect(
|
|
388
|
-
(yield* epics.show("first-epic", root)).data.tasks.map(
|
|
389
|
-
(item) => item.id,
|
|
390
|
-
),
|
|
391
|
-
).not.toContain("alpha")
|
|
392
|
-
expect(
|
|
393
|
-
(yield* epics.show("second-epic", root)).data.tasks.map(
|
|
394
|
-
(item) => item.id,
|
|
395
|
-
),
|
|
396
|
-
).toEqual(["alpha"])
|
|
397
|
-
expect((yield* tasks.show("alpha", root)).data.epic).toBe("second-epic")
|
|
398
|
-
}),
|
|
399
|
-
)
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
test("rejects stale guarded moves before changing any document", async () => {
|
|
403
|
-
const paths = [
|
|
404
|
-
join(root, "tasks/alpha/TASK.md"),
|
|
405
|
-
join(root, "epics/first-epic/EPIC.md"),
|
|
406
|
-
join(root, "epics/second-epic/EPIC.md"),
|
|
407
|
-
]
|
|
408
|
-
const before = await Promise.all(paths.map((path) => Bun.file(path).text()))
|
|
409
|
-
let conflict: unknown
|
|
410
|
-
try {
|
|
411
|
-
await runTestEffect(
|
|
412
|
-
Effect.gen(function* () {
|
|
413
|
-
return yield* (yield* GraphMutationService).moveTask(
|
|
414
|
-
"alpha",
|
|
415
|
-
"second-epic",
|
|
416
|
-
root,
|
|
417
|
-
"0".repeat(64),
|
|
418
|
-
)
|
|
419
|
-
}),
|
|
420
|
-
)
|
|
421
|
-
} catch (error) {
|
|
422
|
-
conflict = error
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
expect(String(conflict)).toContain(
|
|
426
|
-
"Revision conflict for tasks/alpha/TASK.md",
|
|
427
|
-
)
|
|
428
|
-
expect(
|
|
429
|
-
await Promise.all(paths.map((path) => Bun.file(path).text())),
|
|
430
|
-
).toEqual(before)
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
test("refuses a rename that would invalidate a materialized worktree", async () => {
|
|
434
|
-
await mkdir(join(root, "tasks/alpha/code/agency"), { recursive: true })
|
|
435
|
-
await expect(
|
|
436
|
-
runTestEffect(
|
|
437
|
-
Effect.gen(function* () {
|
|
438
|
-
return yield* (yield* GraphMutationService).renameTask(
|
|
439
|
-
"alpha",
|
|
440
|
-
"renamed-alpha",
|
|
441
|
-
root,
|
|
442
|
-
)
|
|
443
|
-
}),
|
|
444
|
-
),
|
|
445
|
-
).rejects.toThrow("materialized worktree")
|
|
446
|
-
expect(await Bun.file(join(root, "tasks/alpha/TASK.md")).exists()).toBe(
|
|
447
|
-
true,
|
|
448
|
-
)
|
|
449
|
-
expect(
|
|
450
|
-
await Bun.file(join(root, "tasks/renamed-alpha/TASK.md")).exists(),
|
|
451
|
-
).toBe(false)
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
test("refuses rename when the parent backlink is inconsistent", async () => {
|
|
455
|
-
await runTestEffect(
|
|
456
|
-
Effect.gen(function* () {
|
|
457
|
-
const epic = yield* (yield* EpicService).show("first-epic", root)
|
|
458
|
-
const parsed = yield* parseFrontmatter(epic.content, epic.path)
|
|
459
|
-
yield* Effect.promise(() =>
|
|
460
|
-
Bun.write(
|
|
461
|
-
epic.path,
|
|
462
|
-
formatMarkdownDocument(
|
|
463
|
-
{
|
|
464
|
-
...epic.data,
|
|
465
|
-
tasks: epic.data.tasks.filter((item) => item.id !== "alpha"),
|
|
466
|
-
},
|
|
467
|
-
parsed.body,
|
|
468
|
-
),
|
|
469
|
-
),
|
|
470
|
-
)
|
|
471
|
-
}),
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
await expect(
|
|
475
|
-
runTestEffect(
|
|
476
|
-
Effect.gen(function* () {
|
|
477
|
-
return yield* (yield* GraphMutationService).renameTask(
|
|
478
|
-
"alpha",
|
|
479
|
-
"renamed-alpha",
|
|
480
|
-
root,
|
|
481
|
-
)
|
|
482
|
-
}),
|
|
483
|
-
),
|
|
484
|
-
).rejects.toThrow("parent epic 'first-epic' does not list it")
|
|
485
|
-
})
|
|
486
|
-
})
|