@aaronshaf/plane 0.1.3 → 0.1.6
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/scripts/check-coverage.ts +2 -2
- package/src/api.ts +67 -59
- package/src/app.ts +78 -0
- package/src/bin.ts +6 -71
- package/src/commands/cycles.ts +104 -85
- package/src/commands/init.ts +57 -55
- package/src/commands/intake.ts +82 -65
- package/src/commands/issue.ts +445 -314
- package/src/commands/issues.ts +51 -43
- package/src/commands/labels.ts +52 -43
- package/src/commands/members.ts +25 -19
- package/src/commands/modules.ts +136 -99
- package/src/commands/pages.ts +58 -49
- package/src/commands/projects.ts +28 -22
- package/src/commands/states.ts +31 -25
- package/src/config.ts +152 -154
- package/src/format.ts +15 -8
- package/src/output.ts +28 -28
- package/src/resolve.ts +88 -53
- package/tests/api.test.ts +178 -155
- package/tests/cycles-extended.test.ts +205 -162
- package/tests/format.test.ts +72 -54
- package/tests/helpers/mock-api.ts +16 -14
- package/tests/intake.test.ts +173 -139
- package/tests/issue-activity.test.ts +191 -158
- package/tests/issue-commands.test.ts +749 -304
- package/tests/issue-comments-worklogs.test.ts +337 -265
- package/tests/issue-links.test.ts +229 -193
- package/tests/modules.test.ts +283 -239
- package/tests/new-schemas.test.ts +203 -183
- package/tests/new-schemas2.test.ts +195 -183
- package/tests/output.test.ts +66 -64
- package/tests/pages.test.ts +122 -108
- package/tests/resolve.test.ts +186 -156
- package/tests/schemas.test.ts +215 -177
|
@@ -1,205 +1,217 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test"
|
|
2
|
-
import { Effect, Schema } from "effect"
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "@/config"
|
|
15
|
-
|
|
16
|
-
async function decode<A, I>(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
4
|
+
WorklogSchema,
|
|
5
|
+
WorklogsResponseSchema,
|
|
6
|
+
IntakeIssueSchema,
|
|
7
|
+
IntakeIssuesResponseSchema,
|
|
8
|
+
PageSchema,
|
|
9
|
+
PagesResponseSchema,
|
|
10
|
+
CommentSchema,
|
|
11
|
+
CommentsResponseSchema,
|
|
12
|
+
CycleIssueSchema,
|
|
13
|
+
CycleIssuesResponseSchema,
|
|
14
|
+
} from "@/config";
|
|
15
|
+
|
|
16
|
+
async function decode<A, I>(
|
|
17
|
+
schema: Schema.Schema<A, I>,
|
|
18
|
+
data: unknown,
|
|
19
|
+
): Promise<A> {
|
|
20
|
+
return Effect.runPromise(
|
|
21
|
+
Schema.decodeUnknown(schema)(data).pipe(
|
|
22
|
+
Effect.mapError((e) => new Error(String(e))),
|
|
23
|
+
),
|
|
24
|
+
);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
describe("WorklogSchema", () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
28
|
+
it("decodes a full worklog", async () => {
|
|
29
|
+
const w = await decode(WorklogSchema, {
|
|
30
|
+
id: "w1",
|
|
31
|
+
description: "Code review",
|
|
32
|
+
duration: 90,
|
|
33
|
+
logged_by_detail: { display_name: "Aaron" },
|
|
34
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
35
|
+
});
|
|
36
|
+
expect(w.duration).toBe(90);
|
|
37
|
+
expect(w.logged_by_detail?.display_name).toBe("Aaron");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("decodes with null description", async () => {
|
|
41
|
+
const w = await decode(WorklogSchema, {
|
|
42
|
+
id: "w2",
|
|
43
|
+
description: null,
|
|
44
|
+
duration: 30,
|
|
45
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
46
|
+
});
|
|
47
|
+
expect(w.description).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rejects missing duration", async () => {
|
|
51
|
+
await expect(
|
|
52
|
+
decode(WorklogSchema, { id: "w3", created_at: "2025-01-15T10:00:00Z" }),
|
|
53
|
+
).rejects.toThrow();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
51
56
|
|
|
52
57
|
describe("WorklogsResponseSchema", () => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
})
|
|
58
|
+
it("decodes results", async () => {
|
|
59
|
+
const resp = await decode(WorklogsResponseSchema, {
|
|
60
|
+
results: [{ id: "w1", duration: 60, created_at: "2025-01-15T10:00:00Z" }],
|
|
61
|
+
});
|
|
62
|
+
expect(resp.results).toHaveLength(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("decodes empty", async () => {
|
|
66
|
+
const resp = await decode(WorklogsResponseSchema, { results: [] });
|
|
67
|
+
expect(resp.results).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
65
70
|
|
|
66
71
|
describe("IntakeIssueSchema", () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
72
|
+
it("decodes a full intake issue", async () => {
|
|
73
|
+
const i = await decode(IntakeIssueSchema, {
|
|
74
|
+
id: "int1",
|
|
75
|
+
issue: "issue-uuid",
|
|
76
|
+
issue_detail: {
|
|
77
|
+
id: "issue-uuid",
|
|
78
|
+
sequence_id: 42,
|
|
79
|
+
name: "Bug report",
|
|
80
|
+
priority: "high",
|
|
81
|
+
},
|
|
82
|
+
status: 0,
|
|
83
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
84
|
+
});
|
|
85
|
+
expect(i.status).toBe(0);
|
|
86
|
+
expect(i.issue_detail?.sequence_id).toBe(42);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("decodes minimal intake issue", async () => {
|
|
90
|
+
const i = await decode(IntakeIssueSchema, {
|
|
91
|
+
id: "int2",
|
|
92
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
93
|
+
});
|
|
94
|
+
expect(i.id).toBe("int2");
|
|
95
|
+
expect(i.status).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("rejects missing id", async () => {
|
|
99
|
+
await expect(
|
|
100
|
+
decode(IntakeIssueSchema, { created_at: "2025-01-15T10:00:00Z" }),
|
|
101
|
+
).rejects.toThrow();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
94
104
|
|
|
95
105
|
describe("IntakeIssuesResponseSchema", () => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
})
|
|
106
|
+
it("decodes results", async () => {
|
|
107
|
+
const resp = await decode(IntakeIssuesResponseSchema, {
|
|
108
|
+
results: [{ id: "int1", created_at: "2025-01-15T10:00:00Z" }],
|
|
109
|
+
});
|
|
110
|
+
expect(resp.results).toHaveLength(1);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
103
113
|
|
|
104
114
|
describe("PageSchema", () => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
})
|
|
115
|
+
it("decodes a page", async () => {
|
|
116
|
+
const p = await decode(PageSchema, {
|
|
117
|
+
id: "pg1",
|
|
118
|
+
name: "Architecture Overview",
|
|
119
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
120
|
+
updated_at: "2025-01-16T10:00:00Z",
|
|
121
|
+
});
|
|
122
|
+
expect(p.name).toBe("Architecture Overview");
|
|
123
|
+
expect(p.updated_at).toBe("2025-01-16T10:00:00Z");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("accepts null description_html", async () => {
|
|
127
|
+
const p = await decode(PageSchema, {
|
|
128
|
+
id: "pg2",
|
|
129
|
+
name: "Empty page",
|
|
130
|
+
description_html: null,
|
|
131
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
132
|
+
});
|
|
133
|
+
expect(p.description_html).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("rejects missing name", async () => {
|
|
137
|
+
await expect(
|
|
138
|
+
decode(PageSchema, { id: "pg3", created_at: "2025-01-15T10:00:00Z" }),
|
|
139
|
+
).rejects.toThrow();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
132
142
|
|
|
133
143
|
describe("PagesResponseSchema", () => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
it("decodes results", async () => {
|
|
145
|
+
const resp = await decode(PagesResponseSchema, {
|
|
146
|
+
results: [
|
|
147
|
+
{ id: "pg1", name: "Arch", created_at: "2025-01-15T10:00:00Z" },
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
expect(resp.results[0].name).toBe("Arch");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
141
153
|
|
|
142
154
|
describe("CommentSchema", () => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
})
|
|
155
|
+
it("decodes a comment", async () => {
|
|
156
|
+
const c = await decode(CommentSchema, {
|
|
157
|
+
id: "c1",
|
|
158
|
+
comment_html: "<p>Hello</p>",
|
|
159
|
+
actor_detail: { display_name: "Aaron" },
|
|
160
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
161
|
+
});
|
|
162
|
+
expect(c.comment_html).toBe("<p>Hello</p>");
|
|
163
|
+
expect(c.actor_detail?.display_name).toBe("Aaron");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("decodes without optional fields", async () => {
|
|
167
|
+
const c = await decode(CommentSchema, {
|
|
168
|
+
id: "c2",
|
|
169
|
+
created_at: "2025-01-15T10:00:00Z",
|
|
170
|
+
});
|
|
171
|
+
expect(c.id).toBe("c2");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("rejects missing id", async () => {
|
|
175
|
+
await expect(
|
|
176
|
+
decode(CommentSchema, { created_at: "2025-01-15T10:00:00Z" }),
|
|
177
|
+
).rejects.toThrow();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
168
180
|
|
|
169
181
|
describe("CommentsResponseSchema", () => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
})
|
|
182
|
+
it("decodes results", async () => {
|
|
183
|
+
const resp = await decode(CommentsResponseSchema, {
|
|
184
|
+
results: [{ id: "c1", created_at: "2025-01-15T10:00:00Z" }],
|
|
185
|
+
});
|
|
186
|
+
expect(resp.results).toHaveLength(1);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
177
189
|
|
|
178
190
|
describe("CycleIssueSchema", () => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
})
|
|
191
|
+
it("decodes with detail", async () => {
|
|
192
|
+
const ci = await decode(CycleIssueSchema, {
|
|
193
|
+
id: "ci1",
|
|
194
|
+
issue: "i1",
|
|
195
|
+
issue_detail: { id: "i1", sequence_id: 5, name: "Fix bug" },
|
|
196
|
+
});
|
|
197
|
+
expect(ci.issue_detail?.sequence_id).toBe(5);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("decodes without detail", async () => {
|
|
201
|
+
const ci = await decode(CycleIssueSchema, { id: "ci2", issue: "i2" });
|
|
202
|
+
expect(ci.issue).toBe("i2");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("rejects missing issue", async () => {
|
|
206
|
+
await expect(decode(CycleIssueSchema, { id: "ci3" })).rejects.toThrow();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
197
209
|
|
|
198
210
|
describe("CycleIssuesResponseSchema", () => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
})
|
|
211
|
+
it("decodes results", async () => {
|
|
212
|
+
const resp = await decode(CycleIssuesResponseSchema, {
|
|
213
|
+
results: [{ id: "ci1", issue: "i1" }],
|
|
214
|
+
});
|
|
215
|
+
expect(resp.results).toHaveLength(1);
|
|
216
|
+
});
|
|
217
|
+
});
|
package/tests/output.test.ts
CHANGED
|
@@ -1,78 +1,80 @@
|
|
|
1
|
-
import { describe, expect, it, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { toXml } from "@/output"
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { toXml } from "@/output";
|
|
3
3
|
|
|
4
4
|
describe("toXml", () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
it("wraps results in <results> root element", () => {
|
|
6
|
+
const out = toXml([{ id: "1", name: "Foo" }]);
|
|
7
|
+
expect(out).toStartWith("<results>");
|
|
8
|
+
expect(out).toEndWith("</results>");
|
|
9
|
+
});
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
it("renders each item as an <item> element with attributes", () => {
|
|
12
|
+
const out = toXml([{ id: "abc", name: "My Project" }]);
|
|
13
|
+
expect(out).toContain('<item id="abc" name="My Project">');
|
|
14
|
+
});
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
it("renders an empty <results> for empty array", () => {
|
|
17
|
+
const out = toXml([]);
|
|
18
|
+
expect(out).toBe("<results>\n\n</results>");
|
|
19
|
+
});
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
it("renders multiple items", () => {
|
|
22
|
+
const out = toXml([
|
|
23
|
+
{ id: "1", name: "A" },
|
|
24
|
+
{ id: "2", name: "B" },
|
|
25
|
+
]);
|
|
26
|
+
expect(out).toContain('id="1"');
|
|
27
|
+
expect(out).toContain('id="2"');
|
|
28
|
+
});
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
it("escapes & in attribute values", () => {
|
|
31
|
+
const out = toXml([{ name: "Canvas & Codegen" }]);
|
|
32
|
+
expect(out).toContain("Canvas & Codegen");
|
|
33
|
+
expect(out).not.toContain("Canvas & Codegen");
|
|
34
|
+
});
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
it("escapes < and > in attribute values", () => {
|
|
37
|
+
const out = toXml([{ name: "<tag>" }]);
|
|
38
|
+
expect(out).toContain("<tag>");
|
|
39
|
+
});
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
it("escapes quotes in attribute values", () => {
|
|
42
|
+
const out = toXml([{ name: 'say "hi"' }]);
|
|
43
|
+
expect(out).toContain(""hi"");
|
|
44
|
+
});
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
it("renders nested objects as child elements", () => {
|
|
47
|
+
const out = toXml([
|
|
48
|
+
{ id: "1", state: { name: "Todo", group: "unstarted" } },
|
|
49
|
+
]);
|
|
50
|
+
expect(out).toContain("<state");
|
|
51
|
+
expect(out).toContain('name="Todo"');
|
|
52
|
+
expect(out).toContain('group="unstarted"');
|
|
53
|
+
});
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
it("renders nested arrays as child elements", () => {
|
|
56
|
+
const out = toXml([{ id: "1", tags: ["a", "b"] }]);
|
|
57
|
+
expect(out).toContain("<tags>");
|
|
58
|
+
expect(out).toContain("</tags>");
|
|
59
|
+
});
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
})
|
|
61
|
+
it("handles null values as empty string in attributes", () => {
|
|
62
|
+
const out = toXml([{ id: "1", color: null }]);
|
|
63
|
+
expect(out).toContain('color=""');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
64
66
|
|
|
65
67
|
describe("argv stripping", () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
it("removes --json from process.argv when present", async () => {
|
|
69
|
+
process.argv.push("--json-test-flag-xyz");
|
|
70
|
+
// The module is already loaded; test that toXml is a function (module loaded ok)
|
|
71
|
+
expect(typeof toXml).toBe("function");
|
|
72
|
+
process.argv.pop();
|
|
73
|
+
});
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
75
|
+
it("jsonMode and xmlMode are booleans", async () => {
|
|
76
|
+
const { jsonMode, xmlMode } = await import("@/output");
|
|
77
|
+
expect(typeof jsonMode).toBe("boolean");
|
|
78
|
+
expect(typeof xmlMode).toBe("boolean");
|
|
79
|
+
});
|
|
80
|
+
});
|