@aaronshaf/plane 0.1.2
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/.github/workflows/ci.yml +36 -0
- package/.github/workflows/publish.yml +36 -0
- package/.husky/pre-commit +7 -0
- package/README.md +120 -0
- package/bin/plane +2 -0
- package/bun.lock +244 -0
- package/package.json +57 -0
- package/scripts/check-coverage.ts +54 -0
- package/scripts/check-file-size.ts +36 -0
- package/src/api.ts +81 -0
- package/src/bin.ts +72 -0
- package/src/commands/cycles.ts +108 -0
- package/src/commands/init.ts +72 -0
- package/src/commands/intake.ts +85 -0
- package/src/commands/issue.ts +409 -0
- package/src/commands/issues.ts +51 -0
- package/src/commands/labels.ts +54 -0
- package/src/commands/members.ts +20 -0
- package/src/commands/modules.ts +129 -0
- package/src/commands/pages.ts +63 -0
- package/src/commands/projects.ts +24 -0
- package/src/commands/states.ts +28 -0
- package/src/config.ts +217 -0
- package/src/format.ts +11 -0
- package/src/resolve.ts +76 -0
- package/tests/api.test.ts +169 -0
- package/tests/cycles-extended.test.ts +183 -0
- package/tests/format.test.ts +60 -0
- package/tests/helpers/mock-api.ts +27 -0
- package/tests/intake.test.ts +157 -0
- package/tests/issue-activity.test.ts +167 -0
- package/tests/issue-commands.test.ts +322 -0
- package/tests/issue-comments-worklogs.test.ts +291 -0
- package/tests/issue-links.test.ts +206 -0
- package/tests/modules.test.ts +255 -0
- package/tests/new-schemas.test.ts +201 -0
- package/tests/new-schemas2.test.ts +205 -0
- package/tests/pages.test.ts +124 -0
- package/tests/resolve.test.ts +178 -0
- package/tests/schemas.test.ts +203 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { http, HttpResponse } from "msw"
|
|
4
|
+
import { setupServer } from "msw/node"
|
|
5
|
+
import { _clearProjectCache } from "@/resolve"
|
|
6
|
+
|
|
7
|
+
const BASE = "http://issue-cmd-test.local"
|
|
8
|
+
const WS = "testws"
|
|
9
|
+
|
|
10
|
+
const PROJECTS = [{ id: "proj-acme", identifier: "ACME", name: "Acme Project" }]
|
|
11
|
+
const ISSUES = [
|
|
12
|
+
{ id: "i1", sequence_id: 29, name: "Migrate Button", priority: "high", state: "s1" },
|
|
13
|
+
{ id: "i2", sequence_id: 30, name: "Migrate Input", priority: "medium", state: "s2" },
|
|
14
|
+
]
|
|
15
|
+
const STATES = [
|
|
16
|
+
{ id: "s-done", name: "Done", group: "completed" },
|
|
17
|
+
{ id: "s-todo", name: "Todo", group: "unstarted" },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const server = setupServer(
|
|
21
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/`, () =>
|
|
22
|
+
HttpResponse.json({ results: PROJECTS }),
|
|
23
|
+
),
|
|
24
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/`, () =>
|
|
25
|
+
HttpResponse.json({ results: ISSUES }),
|
|
26
|
+
),
|
|
27
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/states/`, () =>
|
|
28
|
+
HttpResponse.json({ results: STATES }),
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
beforeAll(() => server.listen({ onUnhandledRequest: "error" }))
|
|
33
|
+
afterAll(() => server.close())
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
_clearProjectCache()
|
|
37
|
+
process.env["PLANE_HOST"] = BASE
|
|
38
|
+
process.env["PLANE_WORKSPACE"] = WS
|
|
39
|
+
process.env["PLANE_API_TOKEN"] = "test-token"
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
server.resetHandlers()
|
|
44
|
+
delete process.env["PLANE_HOST"]
|
|
45
|
+
delete process.env["PLANE_WORKSPACE"]
|
|
46
|
+
delete process.env["PLANE_API_TOKEN"]
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe("issueGet", () => {
|
|
50
|
+
it("prints full JSON for an issue", async () => {
|
|
51
|
+
const { issueGet } = await import("@/commands/issue")
|
|
52
|
+
const logs: string[] = []
|
|
53
|
+
const orig = console.log
|
|
54
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await Effect.runPromise((issueGet as any).handler({ ref: "ACME-29" }))
|
|
58
|
+
} finally {
|
|
59
|
+
console.log = orig
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const output = logs.join("\n")
|
|
63
|
+
const parsed = JSON.parse(output)
|
|
64
|
+
expect(parsed.id).toBe("i1")
|
|
65
|
+
expect(parsed.name).toBe("Migrate Button")
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe("issueUpdate", () => {
|
|
70
|
+
it("updates state", async () => {
|
|
71
|
+
server.use(
|
|
72
|
+
http.patch(
|
|
73
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/`,
|
|
74
|
+
async ({ request }) => {
|
|
75
|
+
const body = (await request.json()) as any
|
|
76
|
+
return HttpResponse.json({
|
|
77
|
+
id: "i1",
|
|
78
|
+
sequence_id: 29,
|
|
79
|
+
name: "Migrate Button",
|
|
80
|
+
priority: "high",
|
|
81
|
+
state: body.state ?? "s1",
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const { issueUpdate } = await import("@/commands/issue")
|
|
88
|
+
const logs: string[] = []
|
|
89
|
+
const orig = console.log
|
|
90
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await Effect.runPromise(
|
|
94
|
+
(issueUpdate as any).handler({
|
|
95
|
+
ref: "ACME-29",
|
|
96
|
+
state: { _tag: "Some", value: "completed" },
|
|
97
|
+
priority: { _tag: "None" },
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
} finally {
|
|
101
|
+
console.log = orig
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
expect(logs.join("\n")).toContain("Updated ACME-29")
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("updates priority", async () => {
|
|
108
|
+
server.use(
|
|
109
|
+
http.patch(
|
|
110
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/`,
|
|
111
|
+
async ({ request }) => {
|
|
112
|
+
const body = (await request.json()) as any
|
|
113
|
+
return HttpResponse.json({
|
|
114
|
+
id: "i1",
|
|
115
|
+
sequence_id: 29,
|
|
116
|
+
name: "Migrate Button",
|
|
117
|
+
priority: body.priority,
|
|
118
|
+
state: "s1",
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const { issueUpdate } = await import("@/commands/issue")
|
|
125
|
+
const logs: string[] = []
|
|
126
|
+
const orig = console.log
|
|
127
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await Effect.runPromise(
|
|
131
|
+
(issueUpdate as any).handler({
|
|
132
|
+
ref: "ACME-29",
|
|
133
|
+
state: { _tag: "None" },
|
|
134
|
+
priority: { _tag: "Some", value: "urgent" },
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
} finally {
|
|
138
|
+
console.log = orig
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
expect(logs.join("\n")).toContain("urgent")
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("fails when nothing to update", async () => {
|
|
145
|
+
const { issueUpdate } = await import("@/commands/issue")
|
|
146
|
+
const result = await Effect.runPromise(
|
|
147
|
+
Effect.either(
|
|
148
|
+
(issueUpdate as any).handler({
|
|
149
|
+
ref: "ACME-29",
|
|
150
|
+
state: { _tag: "None" },
|
|
151
|
+
priority: { _tag: "None" },
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
expect(result._tag).toBe("Left")
|
|
156
|
+
if (result._tag === "Left") {
|
|
157
|
+
expect(result.left.message).toContain("Nothing to update")
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("issueComment", () => {
|
|
163
|
+
it("adds a comment", async () => {
|
|
164
|
+
let postedBody: unknown
|
|
165
|
+
server.use(
|
|
166
|
+
http.post(
|
|
167
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/comments/`,
|
|
168
|
+
async ({ request }) => {
|
|
169
|
+
postedBody = await request.json()
|
|
170
|
+
return HttpResponse.json({ id: "c1" }, { status: 201 })
|
|
171
|
+
},
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const { issueComment } = await import("@/commands/issue")
|
|
176
|
+
const logs: string[] = []
|
|
177
|
+
const orig = console.log
|
|
178
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await Effect.runPromise(
|
|
182
|
+
(issueComment as any).handler({ ref: "ACME-29", text: "Fixed in latest build" }),
|
|
183
|
+
)
|
|
184
|
+
} finally {
|
|
185
|
+
console.log = orig
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
expect((postedBody as any).comment_html).toContain("Fixed in latest build")
|
|
189
|
+
expect(logs.join("\n")).toContain("Comment added to ACME-29")
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("HTML-escapes angle brackets in comment text", async () => {
|
|
193
|
+
let postedBody: unknown
|
|
194
|
+
server.use(
|
|
195
|
+
http.post(
|
|
196
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/comments/`,
|
|
197
|
+
async ({ request }) => {
|
|
198
|
+
postedBody = await request.json()
|
|
199
|
+
return HttpResponse.json({ id: "c2" }, { status: 201 })
|
|
200
|
+
},
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const { issueComment } = await import("@/commands/issue")
|
|
205
|
+
try {
|
|
206
|
+
await Effect.runPromise(
|
|
207
|
+
(issueComment as any).handler({ ref: "ACME-29", text: "<script>alert(1)</script>" }),
|
|
208
|
+
)
|
|
209
|
+
} finally {}
|
|
210
|
+
|
|
211
|
+
expect((postedBody as any).comment_html).toContain("<script>")
|
|
212
|
+
expect((postedBody as any).comment_html).not.toContain("<script>")
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe("issueCreate", () => {
|
|
217
|
+
it("creates an issue with just a title", async () => {
|
|
218
|
+
server.use(
|
|
219
|
+
http.post(
|
|
220
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/`,
|
|
221
|
+
async ({ request }) => {
|
|
222
|
+
const body = (await request.json()) as any
|
|
223
|
+
return HttpResponse.json({
|
|
224
|
+
id: "new-i",
|
|
225
|
+
sequence_id: 99,
|
|
226
|
+
name: body.name,
|
|
227
|
+
priority: "none",
|
|
228
|
+
state: "s1",
|
|
229
|
+
})
|
|
230
|
+
},
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const { issueCreate } = await import("@/commands/issue")
|
|
235
|
+
const logs: string[] = []
|
|
236
|
+
const orig = console.log
|
|
237
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await Effect.runPromise(
|
|
241
|
+
(issueCreate as any).handler({
|
|
242
|
+
project: "ACME",
|
|
243
|
+
title: "New issue",
|
|
244
|
+
priority: { _tag: "None" },
|
|
245
|
+
state: { _tag: "None" },
|
|
246
|
+
}),
|
|
247
|
+
)
|
|
248
|
+
} finally {
|
|
249
|
+
console.log = orig
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
expect(logs.join("\n")).toContain("ACME-99")
|
|
253
|
+
expect(logs.join("\n")).toContain("New issue")
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it("creates an issue with priority and state", async () => {
|
|
257
|
+
server.use(
|
|
258
|
+
http.post(
|
|
259
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/`,
|
|
260
|
+
async ({ request }) => {
|
|
261
|
+
const body = (await request.json()) as any
|
|
262
|
+
return HttpResponse.json({
|
|
263
|
+
id: "new-i2",
|
|
264
|
+
sequence_id: 100,
|
|
265
|
+
name: body.name,
|
|
266
|
+
priority: body.priority,
|
|
267
|
+
state: body.state ?? "s1",
|
|
268
|
+
})
|
|
269
|
+
},
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
const { issueCreate } = await import("@/commands/issue")
|
|
274
|
+
const logs: string[] = []
|
|
275
|
+
const orig = console.log
|
|
276
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
await Effect.runPromise(
|
|
280
|
+
(issueCreate as any).handler({
|
|
281
|
+
project: "ACME",
|
|
282
|
+
title: "High priority issue",
|
|
283
|
+
priority: { _tag: "Some", value: "high" },
|
|
284
|
+
state: { _tag: "Some", value: "completed" },
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
287
|
+
} finally {
|
|
288
|
+
console.log = orig
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
expect(logs.join("\n")).toContain("ACME-100")
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe("issueDelete", () => {
|
|
296
|
+
it("deletes an issue", async () => {
|
|
297
|
+
let deleted = false
|
|
298
|
+
server.use(
|
|
299
|
+
http.delete(
|
|
300
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/`,
|
|
301
|
+
() => {
|
|
302
|
+
deleted = true
|
|
303
|
+
return new HttpResponse(null, { status: 204 })
|
|
304
|
+
},
|
|
305
|
+
),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
const { issueDelete } = await import("@/commands/issue")
|
|
309
|
+
const logs: string[] = []
|
|
310
|
+
const orig = console.log
|
|
311
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
await Effect.runPromise((issueDelete as any).handler({ ref: "ACME-29" }))
|
|
315
|
+
} finally {
|
|
316
|
+
console.log = orig
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
expect(deleted).toBe(true)
|
|
320
|
+
expect(logs.join("\n")).toContain("Deleted ACME-29")
|
|
321
|
+
})
|
|
322
|
+
})
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { http, HttpResponse } from "msw"
|
|
4
|
+
import { setupServer } from "msw/node"
|
|
5
|
+
import { _clearProjectCache } from "@/resolve"
|
|
6
|
+
|
|
7
|
+
const BASE = "http://cw-test.local"
|
|
8
|
+
const WS = "testws"
|
|
9
|
+
|
|
10
|
+
const PROJECTS = [{ id: "proj-acme", identifier: "ACME", name: "Acme Project" }]
|
|
11
|
+
const ISSUES = [{ id: "i1", sequence_id: 29, name: "Migrate Button", priority: "high", state: "s1" }]
|
|
12
|
+
const COMMENTS = [
|
|
13
|
+
{
|
|
14
|
+
id: "c1",
|
|
15
|
+
comment_html: "<p>Fixed in v2</p>",
|
|
16
|
+
actor_detail: { display_name: "Aaron" },
|
|
17
|
+
created_at: "2025-01-15T10:30:00Z",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "c2",
|
|
21
|
+
comment_html: "<p>LGTM</p>",
|
|
22
|
+
actor_detail: { display_name: "Bea" },
|
|
23
|
+
created_at: "2025-01-16T09:00:00Z",
|
|
24
|
+
},
|
|
25
|
+
]
|
|
26
|
+
const WORKLOGS = [
|
|
27
|
+
{
|
|
28
|
+
id: "w1",
|
|
29
|
+
description: "Code review",
|
|
30
|
+
duration: 90,
|
|
31
|
+
logged_by_detail: { display_name: "Aaron" },
|
|
32
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const server = setupServer(
|
|
37
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/`, () =>
|
|
38
|
+
HttpResponse.json({ results: PROJECTS }),
|
|
39
|
+
),
|
|
40
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/`, () =>
|
|
41
|
+
HttpResponse.json({ results: ISSUES }),
|
|
42
|
+
),
|
|
43
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/comments/`, () =>
|
|
44
|
+
HttpResponse.json({ results: COMMENTS }),
|
|
45
|
+
),
|
|
46
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/worklogs/`, () =>
|
|
47
|
+
HttpResponse.json({ results: WORKLOGS }),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
beforeAll(() => server.listen({ onUnhandledRequest: "error" }))
|
|
52
|
+
afterAll(() => server.close())
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
_clearProjectCache()
|
|
56
|
+
process.env["PLANE_HOST"] = BASE
|
|
57
|
+
process.env["PLANE_WORKSPACE"] = WS
|
|
58
|
+
process.env["PLANE_API_TOKEN"] = "test-token"
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
server.resetHandlers()
|
|
63
|
+
delete process.env["PLANE_HOST"]
|
|
64
|
+
delete process.env["PLANE_WORKSPACE"]
|
|
65
|
+
delete process.env["PLANE_API_TOKEN"]
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe("issueCommentsList", () => {
|
|
69
|
+
it("lists comments with author and stripped HTML", async () => {
|
|
70
|
+
const { issueCommentsList } = await import("@/commands/issue")
|
|
71
|
+
const logs: string[] = []
|
|
72
|
+
const orig = console.log
|
|
73
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
74
|
+
try {
|
|
75
|
+
await Effect.runPromise((issueCommentsList as any).handler({ ref: "ACME-29" }))
|
|
76
|
+
} finally {
|
|
77
|
+
console.log = orig
|
|
78
|
+
}
|
|
79
|
+
const output = logs.join("\n")
|
|
80
|
+
expect(output).toContain("c1")
|
|
81
|
+
expect(output).toContain("Aaron")
|
|
82
|
+
expect(output).toContain("Fixed in v2")
|
|
83
|
+
expect(output).not.toContain("<p>")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("shows 'No comments' when empty", async () => {
|
|
87
|
+
server.use(
|
|
88
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/comments/`, () =>
|
|
89
|
+
HttpResponse.json({ results: [] }),
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
const { issueCommentsList } = await import("@/commands/issue")
|
|
93
|
+
const logs: string[] = []
|
|
94
|
+
const orig = console.log
|
|
95
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
96
|
+
try {
|
|
97
|
+
await Effect.runPromise((issueCommentsList as any).handler({ ref: "ACME-29" }))
|
|
98
|
+
} finally {
|
|
99
|
+
console.log = orig
|
|
100
|
+
}
|
|
101
|
+
expect(logs.join("\n")).toBe("No comments")
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("shows '?' for missing actor", async () => {
|
|
105
|
+
server.use(
|
|
106
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/comments/`, () =>
|
|
107
|
+
HttpResponse.json({ results: [{ id: "c3", comment_html: "<p>hi</p>", created_at: "2025-01-17T10:00:00Z" }] }),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
const { issueCommentsList } = await import("@/commands/issue")
|
|
111
|
+
const logs: string[] = []
|
|
112
|
+
const orig = console.log
|
|
113
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
114
|
+
try {
|
|
115
|
+
await Effect.runPromise((issueCommentsList as any).handler({ ref: "ACME-29" }))
|
|
116
|
+
} finally {
|
|
117
|
+
console.log = orig
|
|
118
|
+
}
|
|
119
|
+
expect(logs.join("\n")).toContain("?")
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe("issueCommentUpdate", () => {
|
|
124
|
+
it("updates a comment", async () => {
|
|
125
|
+
let patchedBody: unknown
|
|
126
|
+
server.use(
|
|
127
|
+
http.patch(
|
|
128
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/comments/c1/`,
|
|
129
|
+
async ({ request }) => {
|
|
130
|
+
patchedBody = await request.json()
|
|
131
|
+
return HttpResponse.json({ id: "c1", comment_html: "<p>Updated</p>", created_at: "2025-01-15T10:30:00Z" })
|
|
132
|
+
},
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
const { issueCommentUpdate } = await import("@/commands/issue")
|
|
136
|
+
const logs: string[] = []
|
|
137
|
+
const orig = console.log
|
|
138
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
139
|
+
try {
|
|
140
|
+
await Effect.runPromise(
|
|
141
|
+
(issueCommentUpdate as any).handler({ ref: "ACME-29", commentId: "c1", text: "Updated text" }),
|
|
142
|
+
)
|
|
143
|
+
} finally {
|
|
144
|
+
console.log = orig
|
|
145
|
+
}
|
|
146
|
+
expect((patchedBody as any).comment_html).toContain("Updated text")
|
|
147
|
+
expect(logs.join("\n")).toContain("c1")
|
|
148
|
+
expect(logs.join("\n")).toContain("updated")
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe("issueCommentDelete", () => {
|
|
153
|
+
it("deletes a comment", async () => {
|
|
154
|
+
let deleted = false
|
|
155
|
+
server.use(
|
|
156
|
+
http.delete(
|
|
157
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/comments/c1/`,
|
|
158
|
+
() => {
|
|
159
|
+
deleted = true
|
|
160
|
+
return new HttpResponse(null, { status: 204 })
|
|
161
|
+
},
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
const { issueCommentDelete } = await import("@/commands/issue")
|
|
165
|
+
const logs: string[] = []
|
|
166
|
+
const orig = console.log
|
|
167
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
168
|
+
try {
|
|
169
|
+
await Effect.runPromise(
|
|
170
|
+
(issueCommentDelete as any).handler({ ref: "ACME-29", commentId: "c1" }),
|
|
171
|
+
)
|
|
172
|
+
} finally {
|
|
173
|
+
console.log = orig
|
|
174
|
+
}
|
|
175
|
+
expect(deleted).toBe(true)
|
|
176
|
+
expect(logs.join("\n")).toContain("c1")
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe("issueWorklogsList", () => {
|
|
181
|
+
it("lists worklogs with hours", async () => {
|
|
182
|
+
const { issueWorklogsList } = await import("@/commands/issue")
|
|
183
|
+
const logs: string[] = []
|
|
184
|
+
const orig = console.log
|
|
185
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
186
|
+
try {
|
|
187
|
+
await Effect.runPromise((issueWorklogsList as any).handler({ ref: "ACME-29" }))
|
|
188
|
+
} finally {
|
|
189
|
+
console.log = orig
|
|
190
|
+
}
|
|
191
|
+
const output = logs.join("\n")
|
|
192
|
+
expect(output).toContain("w1")
|
|
193
|
+
expect(output).toContain("1.5h")
|
|
194
|
+
expect(output).toContain("Aaron")
|
|
195
|
+
expect(output).toContain("Code review")
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it("shows 'No worklogs' when empty", async () => {
|
|
199
|
+
server.use(
|
|
200
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/worklogs/`, () =>
|
|
201
|
+
HttpResponse.json({ results: [] }),
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
const { issueWorklogsList } = await import("@/commands/issue")
|
|
205
|
+
const logs: string[] = []
|
|
206
|
+
const orig = console.log
|
|
207
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
208
|
+
try {
|
|
209
|
+
await Effect.runPromise((issueWorklogsList as any).handler({ ref: "ACME-29" }))
|
|
210
|
+
} finally {
|
|
211
|
+
console.log = orig
|
|
212
|
+
}
|
|
213
|
+
expect(logs.join("\n")).toBe("No worklogs")
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe("issueWorklogsAdd", () => {
|
|
218
|
+
it("logs time without description", async () => {
|
|
219
|
+
server.use(
|
|
220
|
+
http.post(
|
|
221
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/worklogs/`,
|
|
222
|
+
async ({ request }) => {
|
|
223
|
+
const body = (await request.json()) as any
|
|
224
|
+
return HttpResponse.json({ id: "w-new", duration: body.duration, created_at: "2025-01-15T10:00:00Z" })
|
|
225
|
+
},
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
const { issueWorklogsAdd } = await import("@/commands/issue")
|
|
229
|
+
const logs: string[] = []
|
|
230
|
+
const orig = console.log
|
|
231
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
232
|
+
try {
|
|
233
|
+
await Effect.runPromise(
|
|
234
|
+
(issueWorklogsAdd as any).handler({ ref: "ACME-29", duration: 60, description: { _tag: "None" } }),
|
|
235
|
+
)
|
|
236
|
+
} finally {
|
|
237
|
+
console.log = orig
|
|
238
|
+
}
|
|
239
|
+
expect(logs.join("\n")).toContain("1.0h")
|
|
240
|
+
expect(logs.join("\n")).toContain("w-new")
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("logs time with description", async () => {
|
|
244
|
+
server.use(
|
|
245
|
+
http.post(
|
|
246
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/worklogs/`,
|
|
247
|
+
async ({ request }) => {
|
|
248
|
+
const body = (await request.json()) as any
|
|
249
|
+
return HttpResponse.json({ id: "w-new2", duration: body.duration, description: body.description, created_at: "2025-01-15T10:00:00Z" })
|
|
250
|
+
},
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
const { issueWorklogsAdd } = await import("@/commands/issue")
|
|
254
|
+
const logs: string[] = []
|
|
255
|
+
const orig = console.log
|
|
256
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
257
|
+
try {
|
|
258
|
+
await Effect.runPromise(
|
|
259
|
+
(issueWorklogsAdd as any).handler({
|
|
260
|
+
ref: "ACME-29",
|
|
261
|
+
duration: 30,
|
|
262
|
+
description: { _tag: "Some", value: "standup" },
|
|
263
|
+
}),
|
|
264
|
+
)
|
|
265
|
+
} finally {
|
|
266
|
+
console.log = orig
|
|
267
|
+
}
|
|
268
|
+
expect(logs.join("\n")).toContain("0.5h")
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it("handles missing logged_by_detail in worklogs list", async () => {
|
|
272
|
+
server.use(
|
|
273
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/worklogs/`, () =>
|
|
274
|
+
HttpResponse.json({
|
|
275
|
+
results: [{ id: "w2", duration: 45, created_at: "2025-01-15T10:00:00Z" }],
|
|
276
|
+
}),
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
const { issueWorklogsList } = await import("@/commands/issue")
|
|
280
|
+
const logs: string[] = []
|
|
281
|
+
const orig = console.log
|
|
282
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
283
|
+
try {
|
|
284
|
+
await Effect.runPromise((issueWorklogsList as any).handler({ ref: "ACME-29" }))
|
|
285
|
+
} finally {
|
|
286
|
+
console.log = orig
|
|
287
|
+
}
|
|
288
|
+
expect(logs.join("\n")).toContain("?")
|
|
289
|
+
expect(logs.join("\n")).toContain("0.8h")
|
|
290
|
+
})
|
|
291
|
+
})
|