@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,206 @@
|
|
|
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://links-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 LINKS = [
|
|
13
|
+
{ id: "lnk1", title: "PR #42", url: "https://github.com/org/repo/pull/42", created_at: "2025-01-15T10:00:00Z" },
|
|
14
|
+
{ id: "lnk2", title: null, url: "https://docs.example.com", created_at: "2025-01-14T10:00:00Z" },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const server = setupServer(
|
|
18
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/`, () =>
|
|
19
|
+
HttpResponse.json({ results: PROJECTS }),
|
|
20
|
+
),
|
|
21
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/`, () =>
|
|
22
|
+
HttpResponse.json({ results: ISSUES }),
|
|
23
|
+
),
|
|
24
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/issue-links/`, () =>
|
|
25
|
+
HttpResponse.json({ results: LINKS }),
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
beforeAll(() => server.listen({ onUnhandledRequest: "error" }))
|
|
30
|
+
afterAll(() => server.close())
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
_clearProjectCache()
|
|
34
|
+
process.env["PLANE_HOST"] = BASE
|
|
35
|
+
process.env["PLANE_WORKSPACE"] = WS
|
|
36
|
+
process.env["PLANE_API_TOKEN"] = "test-token"
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
server.resetHandlers()
|
|
41
|
+
delete process.env["PLANE_HOST"]
|
|
42
|
+
delete process.env["PLANE_WORKSPACE"]
|
|
43
|
+
delete process.env["PLANE_API_TOKEN"]
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe("issueLinkList", () => {
|
|
47
|
+
it("lists links for an issue", async () => {
|
|
48
|
+
const { issueLinkList } = await import("@/commands/issue")
|
|
49
|
+
const logs: string[] = []
|
|
50
|
+
const orig = console.log
|
|
51
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await Effect.runPromise((issueLinkList as any).handler({ ref: "ACME-29" }))
|
|
55
|
+
} finally {
|
|
56
|
+
console.log = orig
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const output = logs.join("\n")
|
|
60
|
+
expect(output).toContain("lnk1")
|
|
61
|
+
expect(output).toContain("PR #42")
|
|
62
|
+
expect(output).toContain("https://github.com/org/repo/pull/42")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("shows '(no title)' for null title", async () => {
|
|
66
|
+
const { issueLinkList } = await import("@/commands/issue")
|
|
67
|
+
const logs: string[] = []
|
|
68
|
+
const orig = console.log
|
|
69
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await Effect.runPromise((issueLinkList as any).handler({ ref: "ACME-29" }))
|
|
73
|
+
} finally {
|
|
74
|
+
console.log = orig
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
expect(logs.join("\n")).toContain("(no title)")
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("shows 'No links' when empty", async () => {
|
|
81
|
+
server.use(
|
|
82
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/issue-links/`, () =>
|
|
83
|
+
HttpResponse.json({ results: [] }),
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const { issueLinkList } = 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((issueLinkList as any).handler({ ref: "ACME-29" }))
|
|
94
|
+
} finally {
|
|
95
|
+
console.log = orig
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
expect(logs.join("\n")).toBe("No links")
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe("issueLinkAdd", () => {
|
|
103
|
+
it("adds a link without title", async () => {
|
|
104
|
+
server.use(
|
|
105
|
+
http.post(
|
|
106
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/issue-links/`,
|
|
107
|
+
async ({ request }) => {
|
|
108
|
+
const body = (await request.json()) as any
|
|
109
|
+
return HttpResponse.json({
|
|
110
|
+
id: "lnk-new",
|
|
111
|
+
url: body.url,
|
|
112
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const { issueLinkAdd } = await import("@/commands/issue")
|
|
119
|
+
const logs: string[] = []
|
|
120
|
+
const orig = console.log
|
|
121
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await Effect.runPromise(
|
|
125
|
+
(issueLinkAdd as any).handler({
|
|
126
|
+
ref: "ACME-29",
|
|
127
|
+
url: "https://example.com",
|
|
128
|
+
title: { _tag: "None" },
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
} finally {
|
|
132
|
+
console.log = orig
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const output = logs.join("\n")
|
|
136
|
+
expect(output).toContain("lnk-new")
|
|
137
|
+
expect(output).toContain("https://example.com")
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("adds a link with title", async () => {
|
|
141
|
+
server.use(
|
|
142
|
+
http.post(
|
|
143
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/issue-links/`,
|
|
144
|
+
async ({ request }) => {
|
|
145
|
+
const body = (await request.json()) as any
|
|
146
|
+
return HttpResponse.json({
|
|
147
|
+
id: "lnk-new2",
|
|
148
|
+
title: body.title,
|
|
149
|
+
url: body.url,
|
|
150
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
151
|
+
})
|
|
152
|
+
},
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const { issueLinkAdd } = await import("@/commands/issue")
|
|
157
|
+
const logs: string[] = []
|
|
158
|
+
const orig = console.log
|
|
159
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await Effect.runPromise(
|
|
163
|
+
(issueLinkAdd as any).handler({
|
|
164
|
+
ref: "ACME-29",
|
|
165
|
+
url: "https://docs.example.com",
|
|
166
|
+
title: { _tag: "Some", value: "Design doc" },
|
|
167
|
+
}),
|
|
168
|
+
)
|
|
169
|
+
} finally {
|
|
170
|
+
console.log = orig
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
expect(logs.join("\n")).toContain("lnk-new2")
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe("issueLinkRemove", () => {
|
|
178
|
+
it("removes a link", async () => {
|
|
179
|
+
let deleted = false
|
|
180
|
+
server.use(
|
|
181
|
+
http.delete(
|
|
182
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/i1/issue-links/lnk1/`,
|
|
183
|
+
() => {
|
|
184
|
+
deleted = true
|
|
185
|
+
return new HttpResponse(null, { status: 204 })
|
|
186
|
+
},
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const { issueLinkRemove } = await import("@/commands/issue")
|
|
191
|
+
const logs: string[] = []
|
|
192
|
+
const orig = console.log
|
|
193
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await Effect.runPromise(
|
|
197
|
+
(issueLinkRemove as any).handler({ ref: "ACME-29", linkId: "lnk1" }),
|
|
198
|
+
)
|
|
199
|
+
} finally {
|
|
200
|
+
console.log = orig
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
expect(deleted).toBe(true)
|
|
204
|
+
expect(logs.join("\n")).toContain("lnk1")
|
|
205
|
+
})
|
|
206
|
+
})
|
|
@@ -0,0 +1,255 @@
|
|
|
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://modules-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 MODULES = [
|
|
13
|
+
{ id: "mod1", name: "Sprint 1", status: "in_progress" },
|
|
14
|
+
{ id: "mod2", name: "Sprint 2", status: "backlog" },
|
|
15
|
+
]
|
|
16
|
+
const MODULE_ISSUES = [
|
|
17
|
+
{
|
|
18
|
+
id: "mi1",
|
|
19
|
+
issue: "i1",
|
|
20
|
+
issue_detail: { id: "i1", sequence_id: 29, name: "Migrate Button" },
|
|
21
|
+
},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const server = setupServer(
|
|
25
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/`, () =>
|
|
26
|
+
HttpResponse.json({ results: PROJECTS }),
|
|
27
|
+
),
|
|
28
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/issues/`, () =>
|
|
29
|
+
HttpResponse.json({ results: ISSUES }),
|
|
30
|
+
),
|
|
31
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/`, () =>
|
|
32
|
+
HttpResponse.json({ results: MODULES }),
|
|
33
|
+
),
|
|
34
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/mod1/module-issues/`, () =>
|
|
35
|
+
HttpResponse.json({ results: MODULE_ISSUES }),
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
beforeAll(() => server.listen({ onUnhandledRequest: "error" }))
|
|
40
|
+
afterAll(() => server.close())
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
_clearProjectCache()
|
|
44
|
+
process.env["PLANE_HOST"] = BASE
|
|
45
|
+
process.env["PLANE_WORKSPACE"] = WS
|
|
46
|
+
process.env["PLANE_API_TOKEN"] = "test-token"
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
server.resetHandlers()
|
|
51
|
+
delete process.env["PLANE_HOST"]
|
|
52
|
+
delete process.env["PLANE_WORKSPACE"]
|
|
53
|
+
delete process.env["PLANE_API_TOKEN"]
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe("modulesList", () => {
|
|
57
|
+
it("lists modules for a project", async () => {
|
|
58
|
+
const { modulesList } = await import("@/commands/modules")
|
|
59
|
+
const logs: string[] = []
|
|
60
|
+
const orig = console.log
|
|
61
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await Effect.runPromise((modulesList as any).handler({ project: "ACME" }))
|
|
65
|
+
} finally {
|
|
66
|
+
console.log = orig
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const output = logs.join("\n")
|
|
70
|
+
expect(output).toContain("mod1")
|
|
71
|
+
expect(output).toContain("Sprint 1")
|
|
72
|
+
expect(output).toContain("in_progress")
|
|
73
|
+
expect(output).toContain("mod2")
|
|
74
|
+
expect(output).toContain("Sprint 2")
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("shows 'No modules found' when empty", async () => {
|
|
78
|
+
server.use(
|
|
79
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/`, () =>
|
|
80
|
+
HttpResponse.json({ results: [] }),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const { modulesList } = await import("@/commands/modules")
|
|
85
|
+
const logs: string[] = []
|
|
86
|
+
const orig = console.log
|
|
87
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await Effect.runPromise((modulesList as any).handler({ project: "ACME" }))
|
|
91
|
+
} finally {
|
|
92
|
+
console.log = orig
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
expect(logs.join("\n")).toBe("No modules found")
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("shows '?' for missing status", async () => {
|
|
99
|
+
server.use(
|
|
100
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/`, () =>
|
|
101
|
+
HttpResponse.json({ results: [{ id: "mod3", name: "Unstarted Sprint" }] }),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const { modulesList } = await import("@/commands/modules")
|
|
106
|
+
const logs: string[] = []
|
|
107
|
+
const orig = console.log
|
|
108
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await Effect.runPromise((modulesList as any).handler({ project: "ACME" }))
|
|
112
|
+
} finally {
|
|
113
|
+
console.log = orig
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
expect(logs.join("\n")).toContain("?")
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe("moduleIssuesList", () => {
|
|
121
|
+
it("lists issues in a module with detail", async () => {
|
|
122
|
+
const { moduleIssuesList } = await import("@/commands/modules")
|
|
123
|
+
const logs: string[] = []
|
|
124
|
+
const orig = console.log
|
|
125
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await Effect.runPromise(
|
|
129
|
+
(moduleIssuesList as any).handler({ project: "ACME", moduleId: "mod1" }),
|
|
130
|
+
)
|
|
131
|
+
} finally {
|
|
132
|
+
console.log = orig
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const output = logs.join("\n")
|
|
136
|
+
expect(output).toContain("ACME-")
|
|
137
|
+
expect(output).toContain("29")
|
|
138
|
+
expect(output).toContain("Migrate Button")
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("falls back to issue UUID when no issue_detail", async () => {
|
|
142
|
+
server.use(
|
|
143
|
+
http.get(
|
|
144
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/mod1/module-issues/`,
|
|
145
|
+
() => HttpResponse.json({ results: [{ id: "mi2", issue: "bare-uuid" }] }),
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
const { moduleIssuesList } = await import("@/commands/modules")
|
|
150
|
+
const logs: string[] = []
|
|
151
|
+
const orig = console.log
|
|
152
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await Effect.runPromise(
|
|
156
|
+
(moduleIssuesList as any).handler({ project: "ACME", moduleId: "mod1" }),
|
|
157
|
+
)
|
|
158
|
+
} finally {
|
|
159
|
+
console.log = orig
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
expect(logs.join("\n")).toContain("bare-uuid")
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it("shows 'No issues in module' when empty", async () => {
|
|
166
|
+
server.use(
|
|
167
|
+
http.get(
|
|
168
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/mod1/module-issues/`,
|
|
169
|
+
() => HttpResponse.json({ results: [] }),
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const { moduleIssuesList } = await import("@/commands/modules")
|
|
174
|
+
const logs: string[] = []
|
|
175
|
+
const orig = console.log
|
|
176
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await Effect.runPromise(
|
|
180
|
+
(moduleIssuesList as any).handler({ project: "ACME", moduleId: "mod1" }),
|
|
181
|
+
)
|
|
182
|
+
} finally {
|
|
183
|
+
console.log = orig
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
expect(logs.join("\n")).toBe("No issues in module")
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe("moduleIssuesAdd", () => {
|
|
191
|
+
it("adds an issue to a module", async () => {
|
|
192
|
+
let postedBody: unknown
|
|
193
|
+
server.use(
|
|
194
|
+
http.post(
|
|
195
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/mod1/module-issues/`,
|
|
196
|
+
async ({ request }) => {
|
|
197
|
+
postedBody = await request.json()
|
|
198
|
+
return HttpResponse.json({ issues: ["i1"] }, { status: 201 })
|
|
199
|
+
},
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const { moduleIssuesAdd } = await import("@/commands/modules")
|
|
204
|
+
const logs: string[] = []
|
|
205
|
+
const orig = console.log
|
|
206
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await Effect.runPromise(
|
|
210
|
+
(moduleIssuesAdd as any).handler({ project: "ACME", moduleId: "mod1", ref: "ACME-29" }),
|
|
211
|
+
)
|
|
212
|
+
} finally {
|
|
213
|
+
console.log = orig
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
expect((postedBody as any).issues).toContain("i1")
|
|
217
|
+
expect(logs.join("\n")).toContain("ACME-29")
|
|
218
|
+
expect(logs.join("\n")).toContain("mod1")
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe("moduleIssuesRemove", () => {
|
|
223
|
+
it("removes a module-issue", async () => {
|
|
224
|
+
let deleted = false
|
|
225
|
+
server.use(
|
|
226
|
+
http.delete(
|
|
227
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/proj-acme/modules/mod1/module-issues/mi1/`,
|
|
228
|
+
() => {
|
|
229
|
+
deleted = true
|
|
230
|
+
return new HttpResponse(null, { status: 204 })
|
|
231
|
+
},
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const { moduleIssuesRemove } = await import("@/commands/modules")
|
|
236
|
+
const logs: string[] = []
|
|
237
|
+
const orig = console.log
|
|
238
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "))
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await Effect.runPromise(
|
|
242
|
+
(moduleIssuesRemove as any).handler({
|
|
243
|
+
project: "ACME",
|
|
244
|
+
moduleId: "mod1",
|
|
245
|
+
moduleIssueId: "mi1",
|
|
246
|
+
}),
|
|
247
|
+
)
|
|
248
|
+
} finally {
|
|
249
|
+
console.log = orig
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
expect(deleted).toBe(true)
|
|
253
|
+
expect(logs.join("\n")).toContain("mi1")
|
|
254
|
+
})
|
|
255
|
+
})
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import { Effect, Schema } from "effect"
|
|
3
|
+
import {
|
|
4
|
+
ActivitySchema,
|
|
5
|
+
ActivitiesResponseSchema,
|
|
6
|
+
IssueLinkSchema,
|
|
7
|
+
IssueLinksResponseSchema,
|
|
8
|
+
ModuleSchema,
|
|
9
|
+
ModulesResponseSchema,
|
|
10
|
+
ModuleIssueSchema,
|
|
11
|
+
ModuleIssuesResponseSchema,
|
|
12
|
+
} from "@/config"
|
|
13
|
+
|
|
14
|
+
async function decode<A, I>(schema: Schema.Schema<A, I>, data: unknown): Promise<A> {
|
|
15
|
+
return Effect.runPromise(
|
|
16
|
+
Schema.decodeUnknown(schema)(data).pipe(Effect.mapError((e) => new Error(String(e)))),
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("ActivitySchema", () => {
|
|
21
|
+
it("decodes a full activity", async () => {
|
|
22
|
+
const a = await decode(ActivitySchema, {
|
|
23
|
+
id: "act1",
|
|
24
|
+
actor_detail: { display_name: "Aaron" },
|
|
25
|
+
field: "state",
|
|
26
|
+
old_value: "Backlog",
|
|
27
|
+
new_value: "In Progress",
|
|
28
|
+
verb: "updated",
|
|
29
|
+
created_at: "2025-01-15T10:30:00Z",
|
|
30
|
+
})
|
|
31
|
+
expect(a.id).toBe("act1")
|
|
32
|
+
expect(a.actor_detail?.display_name).toBe("Aaron")
|
|
33
|
+
expect(a.field).toBe("state")
|
|
34
|
+
expect(a.old_value).toBe("Backlog")
|
|
35
|
+
expect(a.new_value).toBe("In Progress")
|
|
36
|
+
expect(a.created_at).toBe("2025-01-15T10:30:00Z")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("decodes activity with null field values", async () => {
|
|
40
|
+
const a = await decode(ActivitySchema, {
|
|
41
|
+
id: "act2",
|
|
42
|
+
field: null,
|
|
43
|
+
old_value: null,
|
|
44
|
+
new_value: null,
|
|
45
|
+
created_at: "2025-01-15T10:30:00Z",
|
|
46
|
+
})
|
|
47
|
+
expect(a.field).toBeNull()
|
|
48
|
+
expect(a.old_value).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("decodes minimal activity (no optional fields)", async () => {
|
|
52
|
+
const a = await decode(ActivitySchema, {
|
|
53
|
+
id: "act3",
|
|
54
|
+
created_at: "2025-01-15T10:30:00Z",
|
|
55
|
+
})
|
|
56
|
+
expect(a.id).toBe("act3")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("rejects missing id", async () => {
|
|
60
|
+
await expect(
|
|
61
|
+
decode(ActivitySchema, { created_at: "2025-01-15T10:30:00Z" }),
|
|
62
|
+
).rejects.toThrow()
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe("ActivitiesResponseSchema", () => {
|
|
67
|
+
it("decodes results array", async () => {
|
|
68
|
+
const resp = await decode(ActivitiesResponseSchema, {
|
|
69
|
+
results: [
|
|
70
|
+
{ id: "a1", created_at: "2025-01-15T10:30:00Z" },
|
|
71
|
+
{ id: "a2", created_at: "2025-01-16T10:30:00Z" },
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
expect(resp.results).toHaveLength(2)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("decodes empty results", async () => {
|
|
78
|
+
const resp = await decode(ActivitiesResponseSchema, { results: [] })
|
|
79
|
+
expect(resp.results).toHaveLength(0)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe("IssueLinkSchema", () => {
|
|
84
|
+
it("decodes a full link", async () => {
|
|
85
|
+
const l = await decode(IssueLinkSchema, {
|
|
86
|
+
id: "link1",
|
|
87
|
+
title: "PR #42",
|
|
88
|
+
url: "https://github.com/org/repo/pull/42",
|
|
89
|
+
created_at: "2025-01-15T10:30:00Z",
|
|
90
|
+
})
|
|
91
|
+
expect(l.id).toBe("link1")
|
|
92
|
+
expect(l.title).toBe("PR #42")
|
|
93
|
+
expect(l.url).toBe("https://github.com/org/repo/pull/42")
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("decodes link with null title", async () => {
|
|
97
|
+
const l = await decode(IssueLinkSchema, {
|
|
98
|
+
id: "link2",
|
|
99
|
+
title: null,
|
|
100
|
+
url: "https://example.com",
|
|
101
|
+
created_at: "2025-01-15T10:30:00Z",
|
|
102
|
+
})
|
|
103
|
+
expect(l.title).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("rejects missing url", async () => {
|
|
107
|
+
await expect(
|
|
108
|
+
decode(IssueLinkSchema, { id: "link3", created_at: "2025-01-15T10:30:00Z" }),
|
|
109
|
+
).rejects.toThrow()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe("IssueLinksResponseSchema", () => {
|
|
114
|
+
it("decodes results", async () => {
|
|
115
|
+
const resp = await decode(IssueLinksResponseSchema, {
|
|
116
|
+
results: [
|
|
117
|
+
{ id: "l1", url: "https://example.com/1", created_at: "2025-01-15T10:00:00Z" },
|
|
118
|
+
],
|
|
119
|
+
})
|
|
120
|
+
expect(resp.results).toHaveLength(1)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("decodes empty results", async () => {
|
|
124
|
+
const resp = await decode(IssueLinksResponseSchema, { results: [] })
|
|
125
|
+
expect(resp.results).toHaveLength(0)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe("ModuleSchema", () => {
|
|
130
|
+
it("decodes a module", async () => {
|
|
131
|
+
const m = await decode(ModuleSchema, { id: "mod1", name: "Sprint 1" })
|
|
132
|
+
expect(m.id).toBe("mod1")
|
|
133
|
+
expect(m.name).toBe("Sprint 1")
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("accepts optional status and description", async () => {
|
|
137
|
+
const m = await decode(ModuleSchema, {
|
|
138
|
+
id: "mod1",
|
|
139
|
+
name: "Sprint 1",
|
|
140
|
+
status: "in_progress",
|
|
141
|
+
description: "Focus on migration",
|
|
142
|
+
})
|
|
143
|
+
expect(m.status).toBe("in_progress")
|
|
144
|
+
expect(m.description).toBe("Focus on migration")
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("accepts null description", async () => {
|
|
148
|
+
const m = await decode(ModuleSchema, { id: "mod1", name: "Sprint 1", description: null })
|
|
149
|
+
expect(m.description).toBeNull()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it("rejects missing name", async () => {
|
|
153
|
+
await expect(decode(ModuleSchema, { id: "mod1" })).rejects.toThrow()
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe("ModulesResponseSchema", () => {
|
|
158
|
+
it("decodes results", async () => {
|
|
159
|
+
const resp = await decode(ModulesResponseSchema, {
|
|
160
|
+
results: [
|
|
161
|
+
{ id: "m1", name: "Sprint 1" },
|
|
162
|
+
{ id: "m2", name: "Sprint 2" },
|
|
163
|
+
],
|
|
164
|
+
})
|
|
165
|
+
expect(resp.results).toHaveLength(2)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe("ModuleIssueSchema", () => {
|
|
170
|
+
it("decodes a module issue with detail", async () => {
|
|
171
|
+
const mi = await decode(ModuleIssueSchema, {
|
|
172
|
+
id: "mi1",
|
|
173
|
+
issue: "issue-uuid",
|
|
174
|
+
issue_detail: { id: "issue-uuid", sequence_id: 29, name: "Migrate Button" },
|
|
175
|
+
})
|
|
176
|
+
expect(mi.id).toBe("mi1")
|
|
177
|
+
expect(mi.issue_detail?.sequence_id).toBe(29)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("decodes without issue_detail", async () => {
|
|
181
|
+
const mi = await decode(ModuleIssueSchema, {
|
|
182
|
+
id: "mi1",
|
|
183
|
+
issue: "issue-uuid",
|
|
184
|
+
})
|
|
185
|
+
expect(mi.issue).toBe("issue-uuid")
|
|
186
|
+
expect(mi.issue_detail).toBeUndefined()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it("rejects missing issue", async () => {
|
|
190
|
+
await expect(decode(ModuleIssueSchema, { id: "mi1" })).rejects.toThrow()
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe("ModuleIssuesResponseSchema", () => {
|
|
195
|
+
it("decodes results", async () => {
|
|
196
|
+
const resp = await decode(ModuleIssuesResponseSchema, {
|
|
197
|
+
results: [{ id: "mi1", issue: "uuid1" }],
|
|
198
|
+
})
|
|
199
|
+
expect(resp.results).toHaveLength(1)
|
|
200
|
+
})
|
|
201
|
+
})
|