@cliangdev/flux-plugin 0.3.1-dev.bdbaeae → 0.3.1
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/commands/dashboard.md +1 -1
- package/package.json +1 -3
- package/src/server/adapters/factory.ts +28 -6
- package/src/server/adapters/types.ts +1 -1
- package/src/server/index.ts +0 -2
- package/src/server/tools/__tests__/mcp-interface.test.ts +0 -6
- package/src/server/tools/index.ts +1 -2
- package/src/server/tools/init-project.ts +12 -26
- package/src/server/adapters/github/__tests__/criteria-deps.test.ts +0 -579
- package/src/server/adapters/github/__tests__/documents-stats.test.ts +0 -789
- package/src/server/adapters/github/__tests__/epic-task-crud.test.ts +0 -1072
- package/src/server/adapters/github/__tests__/foundation.test.ts +0 -537
- package/src/server/adapters/github/__tests__/index-store.test.ts +0 -319
- package/src/server/adapters/github/__tests__/prd-crud.test.ts +0 -836
- package/src/server/adapters/github/adapter.ts +0 -1552
- package/src/server/adapters/github/client.ts +0 -33
- package/src/server/adapters/github/config.ts +0 -59
- package/src/server/adapters/github/helpers/criteria.ts +0 -157
- package/src/server/adapters/github/helpers/index-store.ts +0 -75
- package/src/server/adapters/github/helpers/meta.ts +0 -26
- package/src/server/adapters/github/index.ts +0 -5
- package/src/server/adapters/github/mappers/epic.ts +0 -21
- package/src/server/adapters/github/mappers/index.ts +0 -15
- package/src/server/adapters/github/mappers/prd.ts +0 -50
- package/src/server/adapters/github/mappers/task.ts +0 -37
- package/src/server/adapters/github/types.ts +0 -27
- package/src/server/tools/__tests__/z-configure-github.test.ts +0 -509
- package/src/server/tools/__tests__/z-init-project.test.ts +0 -168
- package/src/server/tools/configure-github.ts +0 -411
|
@@ -1,789 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
|
|
5
|
-
const TEST_DIR = `${realpathSync(tmpdir())}/flux-docs-stats-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
6
|
-
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
7
|
-
|
|
8
|
-
let mockIssuesGet: any = null;
|
|
9
|
-
let mockIssuesList: any[] = [];
|
|
10
|
-
let mockGetContent: any = null;
|
|
11
|
-
let mockGetContentByPath: Record<string, any> = {};
|
|
12
|
-
let mockIssuesUpdate: any = null;
|
|
13
|
-
let lastUpdateParams: any = null;
|
|
14
|
-
let lastPutParams: any = null;
|
|
15
|
-
let lastDeleteParams: any = null;
|
|
16
|
-
let issueNodeIdCounter = 0;
|
|
17
|
-
|
|
18
|
-
mock.module("@octokit/rest", () => ({
|
|
19
|
-
Octokit: class MockOctokit {
|
|
20
|
-
repos = {
|
|
21
|
-
getContent: async (_params: any) => {
|
|
22
|
-
if (mockGetContent === null) {
|
|
23
|
-
const err: any = new Error("Not Found");
|
|
24
|
-
err.status = 404;
|
|
25
|
-
throw err;
|
|
26
|
-
}
|
|
27
|
-
return mockGetContent;
|
|
28
|
-
},
|
|
29
|
-
createOrUpdateFileContents: async (params: any) => {
|
|
30
|
-
lastPutParams = params;
|
|
31
|
-
return { data: { content: { sha: "new-sha" } } };
|
|
32
|
-
},
|
|
33
|
-
deleteFile: async (params: any) => {
|
|
34
|
-
lastDeleteParams = params;
|
|
35
|
-
return { data: {} };
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
issues = {
|
|
39
|
-
get: async (_params: any) => {
|
|
40
|
-
if (mockIssuesGet === null) {
|
|
41
|
-
const err: any = new Error("Not Found");
|
|
42
|
-
err.status = 404;
|
|
43
|
-
throw err;
|
|
44
|
-
}
|
|
45
|
-
return { data: mockIssuesGet };
|
|
46
|
-
},
|
|
47
|
-
listForRepo: async (_params: any) => {
|
|
48
|
-
return { data: mockIssuesList };
|
|
49
|
-
},
|
|
50
|
-
update: async (params: any) => {
|
|
51
|
-
lastUpdateParams = params;
|
|
52
|
-
return { data: mockIssuesUpdate ?? mockIssuesGet };
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
mock.module("@octokit/graphql", () => ({
|
|
59
|
-
graphql: {
|
|
60
|
-
defaults: (_opts: any) => {
|
|
61
|
-
return async (
|
|
62
|
-
_query: string,
|
|
63
|
-
_variables?: Record<string, unknown>,
|
|
64
|
-
) => ({});
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
}));
|
|
68
|
-
|
|
69
|
-
function makeIssue(
|
|
70
|
-
overrides: Partial<{
|
|
71
|
-
number: number;
|
|
72
|
-
title: string;
|
|
73
|
-
body: string;
|
|
74
|
-
labels: string[];
|
|
75
|
-
state: "open" | "closed";
|
|
76
|
-
created_at: string;
|
|
77
|
-
updated_at: string;
|
|
78
|
-
node_id: string;
|
|
79
|
-
}> = {},
|
|
80
|
-
) {
|
|
81
|
-
issueNodeIdCounter++;
|
|
82
|
-
const number = overrides.number ?? issueNodeIdCounter;
|
|
83
|
-
return {
|
|
84
|
-
number,
|
|
85
|
-
title: overrides.title ?? "Test Issue",
|
|
86
|
-
body: overrides.body ?? "",
|
|
87
|
-
labels: (overrides.labels ?? ["flux:prd", "status:draft"]).map((name) => ({
|
|
88
|
-
name,
|
|
89
|
-
})),
|
|
90
|
-
state: overrides.state ?? "open",
|
|
91
|
-
created_at: overrides.created_at ?? "2026-01-01T00:00:00Z",
|
|
92
|
-
updated_at: overrides.updated_at ?? "2026-01-01T00:00:00Z",
|
|
93
|
-
node_id: overrides.node_id ?? `MDU6SXNzdWU${number}`,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function makeAdapter() {
|
|
98
|
-
const { GitHubAdapter } = require("../adapter.js");
|
|
99
|
-
const adapter = new GitHubAdapter({
|
|
100
|
-
token: "ghp_test",
|
|
101
|
-
owner: "test-owner",
|
|
102
|
-
repo: "test-repo",
|
|
103
|
-
projectId: "PVT_kwDO123",
|
|
104
|
-
refPrefix: "FP",
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
(adapter as any).client.rest.issues = {
|
|
108
|
-
get: async (_params: any) => {
|
|
109
|
-
if (mockIssuesGet === null) {
|
|
110
|
-
const err: any = new Error("Not Found");
|
|
111
|
-
err.status = 404;
|
|
112
|
-
throw err;
|
|
113
|
-
}
|
|
114
|
-
return { data: mockIssuesGet };
|
|
115
|
-
},
|
|
116
|
-
listForRepo: async (_params: any) => {
|
|
117
|
-
return { data: mockIssuesList };
|
|
118
|
-
},
|
|
119
|
-
update: async (params: any) => {
|
|
120
|
-
lastUpdateParams = params;
|
|
121
|
-
return { data: mockIssuesUpdate ?? mockIssuesGet };
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
(adapter as any).client.rest.repos = {
|
|
126
|
-
getContent: async (params: any) => {
|
|
127
|
-
const path = params.path;
|
|
128
|
-
if (path in mockGetContentByPath) {
|
|
129
|
-
const val = mockGetContentByPath[path];
|
|
130
|
-
if (val === null) {
|
|
131
|
-
const err: any = new Error("Not Found");
|
|
132
|
-
err.status = 404;
|
|
133
|
-
throw err;
|
|
134
|
-
}
|
|
135
|
-
return val;
|
|
136
|
-
}
|
|
137
|
-
if (mockGetContent === null) {
|
|
138
|
-
const err: any = new Error("Not Found");
|
|
139
|
-
err.status = 404;
|
|
140
|
-
throw err;
|
|
141
|
-
}
|
|
142
|
-
return mockGetContent;
|
|
143
|
-
},
|
|
144
|
-
createOrUpdateFileContents: async (params: any) => {
|
|
145
|
-
lastPutParams = params;
|
|
146
|
-
return { data: { content: { sha: "new-sha" } } };
|
|
147
|
-
},
|
|
148
|
-
deleteFile: async (params: any) => {
|
|
149
|
-
lastDeleteParams = params;
|
|
150
|
-
return { data: {} };
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
(adapter as any).client.rest.request = async (url: string, _params: any) => {
|
|
155
|
-
if (url.includes("sub_issues") && url.startsWith("GET")) {
|
|
156
|
-
return { data: [] };
|
|
157
|
-
}
|
|
158
|
-
if (url.includes("sub_issues") && url.startsWith("POST")) {
|
|
159
|
-
return { data: {} };
|
|
160
|
-
}
|
|
161
|
-
throw new Error(`Unmocked request: ${url}`);
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
return adapter;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
describe("GitHubAdapter Documents", () => {
|
|
168
|
-
beforeEach(() => {
|
|
169
|
-
mockIssuesGet = null;
|
|
170
|
-
mockIssuesList = [];
|
|
171
|
-
mockGetContent = null;
|
|
172
|
-
mockGetContentByPath = {};
|
|
173
|
-
mockIssuesUpdate = null;
|
|
174
|
-
lastUpdateParams = null;
|
|
175
|
-
lastPutParams = null;
|
|
176
|
-
lastDeleteParams = null;
|
|
177
|
-
issueNodeIdCounter = 0;
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
afterEach(() => {});
|
|
181
|
-
|
|
182
|
-
describe("saveDocument", () => {
|
|
183
|
-
test("creates file at correct path and returns Document with blob URL", async () => {
|
|
184
|
-
const prdIssue = makeIssue({
|
|
185
|
-
number: 6,
|
|
186
|
-
labels: ["flux:prd", "status:draft"],
|
|
187
|
-
body: '<!-- flux-meta\n{"ref":"FP-P6"}\n-->',
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
mockGetContent = {
|
|
191
|
-
data: {
|
|
192
|
-
sha: "sha1",
|
|
193
|
-
content: Buffer.from(JSON.stringify({ "FP-P6": 6 })).toString(
|
|
194
|
-
"base64",
|
|
195
|
-
),
|
|
196
|
-
encoding: "base64",
|
|
197
|
-
},
|
|
198
|
-
};
|
|
199
|
-
mockIssuesGet = prdIssue;
|
|
200
|
-
mockGetContentByPath["prds/fp-p6/architecture.md"] = null;
|
|
201
|
-
|
|
202
|
-
const adapter = makeAdapter();
|
|
203
|
-
|
|
204
|
-
const capturedGetParams: any[] = [];
|
|
205
|
-
(adapter as any).client.rest.repos.getContent = async (params: any) => {
|
|
206
|
-
capturedGetParams.push(params);
|
|
207
|
-
if (params.path === ".flux/github-index.json") {
|
|
208
|
-
return {
|
|
209
|
-
data: {
|
|
210
|
-
sha: "sha1",
|
|
211
|
-
content: Buffer.from(JSON.stringify({ "FP-P6": 6 })).toString(
|
|
212
|
-
"base64",
|
|
213
|
-
),
|
|
214
|
-
encoding: "base64",
|
|
215
|
-
},
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
if (params.path === "prds/fp-p6/architecture.md") {
|
|
219
|
-
const err: any = new Error("Not Found");
|
|
220
|
-
err.status = 404;
|
|
221
|
-
throw err;
|
|
222
|
-
}
|
|
223
|
-
const err: any = new Error("Not Found");
|
|
224
|
-
err.status = 404;
|
|
225
|
-
throw err;
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const doc = await adapter.saveDocument({
|
|
229
|
-
prdRef: "FP-P6",
|
|
230
|
-
filename: "architecture.md",
|
|
231
|
-
content: "# Architecture\n\nDetails here.",
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
expect(lastPutParams.path).toBe("prds/fp-p6/architecture.md");
|
|
235
|
-
expect(lastPutParams.content).toBe(
|
|
236
|
-
Buffer.from("# Architecture\n\nDetails here.").toString("base64"),
|
|
237
|
-
);
|
|
238
|
-
expect(doc.url).toBe(
|
|
239
|
-
"https://github.com/test-owner/test-repo/blob/main/prds/fp-p6/architecture.md",
|
|
240
|
-
);
|
|
241
|
-
expect(doc.prdRef).toBe("FP-P6");
|
|
242
|
-
expect(doc.filename).toBe("architecture.md");
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test("updates existing file using current SHA (no conflict error)", async () => {
|
|
246
|
-
const prdIssue = makeIssue({
|
|
247
|
-
number: 6,
|
|
248
|
-
labels: ["flux:prd", "status:draft"],
|
|
249
|
-
body: '<!-- flux-meta\n{"ref":"FP-P6"}\n-->\n\n## Supporting Documents\n\n- [architecture.md](https://github.com/test-owner/test-repo/blob/main/prds/fp-p6/architecture.md)',
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
mockIssuesGet = prdIssue;
|
|
253
|
-
|
|
254
|
-
const adapter = makeAdapter();
|
|
255
|
-
|
|
256
|
-
(adapter as any).client.rest.repos.getContent = async (params: any) => {
|
|
257
|
-
if (params.path === ".flux/github-index.json") {
|
|
258
|
-
return {
|
|
259
|
-
data: {
|
|
260
|
-
sha: "sha-index",
|
|
261
|
-
content: Buffer.from(JSON.stringify({ "FP-P6": 6 })).toString(
|
|
262
|
-
"base64",
|
|
263
|
-
),
|
|
264
|
-
encoding: "base64",
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
if (params.path === "prds/fp-p6/architecture.md") {
|
|
269
|
-
return {
|
|
270
|
-
data: {
|
|
271
|
-
sha: "existing-file-sha",
|
|
272
|
-
content: Buffer.from("# Old Architecture").toString("base64"),
|
|
273
|
-
encoding: "base64",
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
const err: any = new Error("Not Found");
|
|
278
|
-
err.status = 404;
|
|
279
|
-
throw err;
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
const doc = await adapter.saveDocument({
|
|
283
|
-
prdRef: "FP-P6",
|
|
284
|
-
filename: "architecture.md",
|
|
285
|
-
content: "# New Architecture",
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
expect(lastPutParams.sha).toBe("existing-file-sha");
|
|
289
|
-
expect(doc.filename).toBe("architecture.md");
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test("adds link to PRD issue body under Supporting Documents", async () => {
|
|
293
|
-
const prdIssue = makeIssue({
|
|
294
|
-
number: 6,
|
|
295
|
-
labels: ["flux:prd", "status:draft"],
|
|
296
|
-
body: 'Some description.\n\n<!-- flux-meta\n{"ref":"FP-P6"}\n-->',
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
mockIssuesGet = prdIssue;
|
|
300
|
-
|
|
301
|
-
const adapter = makeAdapter();
|
|
302
|
-
|
|
303
|
-
(adapter as any).client.rest.repos.getContent = async (params: any) => {
|
|
304
|
-
if (params.path === ".flux/github-index.json") {
|
|
305
|
-
return {
|
|
306
|
-
data: {
|
|
307
|
-
sha: "sha-index",
|
|
308
|
-
content: Buffer.from(JSON.stringify({ "FP-P6": 6 })).toString(
|
|
309
|
-
"base64",
|
|
310
|
-
),
|
|
311
|
-
encoding: "base64",
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
if (params.path === "prds/fp-p6/notes.md") {
|
|
316
|
-
const err: any = new Error("Not Found");
|
|
317
|
-
err.status = 404;
|
|
318
|
-
throw err;
|
|
319
|
-
}
|
|
320
|
-
const err: any = new Error("Not Found");
|
|
321
|
-
err.status = 404;
|
|
322
|
-
throw err;
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
await adapter.saveDocument({
|
|
326
|
-
prdRef: "FP-P6",
|
|
327
|
-
filename: "notes.md",
|
|
328
|
-
content: "# Notes",
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
expect(lastUpdateParams).not.toBeNull();
|
|
332
|
-
expect(lastUpdateParams.body).toContain("## Supporting Documents");
|
|
333
|
-
expect(lastUpdateParams.body).toContain("notes.md");
|
|
334
|
-
expect(lastUpdateParams.body).toContain(
|
|
335
|
-
"https://github.com/test-owner/test-repo/blob/main/prds/fp-p6/notes.md",
|
|
336
|
-
);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
test("throws descriptive error on unknown PRD ref", async () => {
|
|
340
|
-
mockGetContent = null;
|
|
341
|
-
mockIssuesList = [];
|
|
342
|
-
|
|
343
|
-
const adapter = makeAdapter();
|
|
344
|
-
|
|
345
|
-
await expect(
|
|
346
|
-
adapter.saveDocument({
|
|
347
|
-
prdRef: "FP-P999",
|
|
348
|
-
filename: "missing.md",
|
|
349
|
-
content: "# Missing",
|
|
350
|
-
}),
|
|
351
|
-
).rejects.toThrow("FP-P999");
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
describe("deleteDocument", () => {
|
|
356
|
-
test("deletes file and removes link from PRD body", async () => {
|
|
357
|
-
const existingBody =
|
|
358
|
-
'Description.\n\n<!-- flux-meta\n{"ref":"FP-P6"}\n-->\n\n## Supporting Documents\n\n- [architecture.md](https://github.com/test-owner/test-repo/blob/main/prds/fp-p6/architecture.md)\n- [notes.md](https://github.com/test-owner/test-repo/blob/main/prds/fp-p6/notes.md)';
|
|
359
|
-
|
|
360
|
-
const prdIssue = makeIssue({
|
|
361
|
-
number: 6,
|
|
362
|
-
labels: ["flux:prd", "status:draft"],
|
|
363
|
-
body: existingBody,
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
mockIssuesGet = prdIssue;
|
|
367
|
-
|
|
368
|
-
const adapter = makeAdapter();
|
|
369
|
-
|
|
370
|
-
(adapter as any).client.rest.repos.getContent = async (params: any) => {
|
|
371
|
-
if (params.path === ".flux/github-index.json") {
|
|
372
|
-
return {
|
|
373
|
-
data: {
|
|
374
|
-
sha: "sha-index",
|
|
375
|
-
content: Buffer.from(JSON.stringify({ "FP-P6": 6 })).toString(
|
|
376
|
-
"base64",
|
|
377
|
-
),
|
|
378
|
-
encoding: "base64",
|
|
379
|
-
},
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
if (params.path === "prds/fp-p6/architecture.md") {
|
|
383
|
-
return {
|
|
384
|
-
data: {
|
|
385
|
-
sha: "file-sha-to-delete",
|
|
386
|
-
content: Buffer.from("# Architecture").toString("base64"),
|
|
387
|
-
encoding: "base64",
|
|
388
|
-
},
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
const err: any = new Error("Not Found");
|
|
392
|
-
err.status = 404;
|
|
393
|
-
throw err;
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
await adapter.deleteDocument("FP-P6", "architecture.md");
|
|
397
|
-
|
|
398
|
-
expect(lastDeleteParams.path).toBe("prds/fp-p6/architecture.md");
|
|
399
|
-
expect(lastDeleteParams.sha).toBe("file-sha-to-delete");
|
|
400
|
-
expect(lastUpdateParams).not.toBeNull();
|
|
401
|
-
expect(lastUpdateParams.body).not.toContain("architecture.md");
|
|
402
|
-
expect(lastUpdateParams.body).toContain("notes.md");
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
describe("getDocuments", () => {
|
|
407
|
-
test("returns empty array when PRD has no documents folder (404)", async () => {
|
|
408
|
-
const prdIssue = makeIssue({
|
|
409
|
-
number: 6,
|
|
410
|
-
labels: ["flux:prd", "status:draft"],
|
|
411
|
-
body: '<!-- flux-meta\n{"ref":"FP-P6"}\n-->',
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
mockIssuesGet = prdIssue;
|
|
415
|
-
|
|
416
|
-
const adapter = makeAdapter();
|
|
417
|
-
|
|
418
|
-
(adapter as any).client.rest.repos.getContent = async (params: any) => {
|
|
419
|
-
if (params.path === ".flux/github-index.json") {
|
|
420
|
-
return {
|
|
421
|
-
data: {
|
|
422
|
-
sha: "sha-index",
|
|
423
|
-
content: Buffer.from(JSON.stringify({ "FP-P6": 6 })).toString(
|
|
424
|
-
"base64",
|
|
425
|
-
),
|
|
426
|
-
encoding: "base64",
|
|
427
|
-
},
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
const err: any = new Error("Not Found");
|
|
431
|
-
err.status = 404;
|
|
432
|
-
throw err;
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
const docs = await adapter.getDocuments("FP-P6");
|
|
436
|
-
expect(docs).toEqual([]);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
test("returns correct documents with filename, content, and blob URL", async () => {
|
|
440
|
-
const adapter = makeAdapter();
|
|
441
|
-
|
|
442
|
-
(adapter as any).client.rest.repos.getContent = async (params: any) => {
|
|
443
|
-
if (params.path === ".flux/github-index.json") {
|
|
444
|
-
const err: any = new Error("Not Found");
|
|
445
|
-
err.status = 404;
|
|
446
|
-
throw err;
|
|
447
|
-
}
|
|
448
|
-
if (params.path === "prds/fp-p6") {
|
|
449
|
-
return {
|
|
450
|
-
data: [
|
|
451
|
-
{
|
|
452
|
-
type: "file",
|
|
453
|
-
name: "architecture.md",
|
|
454
|
-
path: "prds/fp-p6/architecture.md",
|
|
455
|
-
sha: "sha-arch",
|
|
456
|
-
download_url: "https://raw.githubusercontent.com/...",
|
|
457
|
-
},
|
|
458
|
-
{
|
|
459
|
-
type: "file",
|
|
460
|
-
name: "notes.md",
|
|
461
|
-
path: "prds/fp-p6/notes.md",
|
|
462
|
-
sha: "sha-notes",
|
|
463
|
-
download_url: "https://raw.githubusercontent.com/...",
|
|
464
|
-
},
|
|
465
|
-
],
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
if (params.path === "prds/fp-p6/architecture.md") {
|
|
469
|
-
return {
|
|
470
|
-
data: {
|
|
471
|
-
sha: "sha-arch",
|
|
472
|
-
content: Buffer.from("# Architecture").toString("base64"),
|
|
473
|
-
encoding: "base64",
|
|
474
|
-
},
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
if (params.path === "prds/fp-p6/notes.md") {
|
|
478
|
-
return {
|
|
479
|
-
data: {
|
|
480
|
-
sha: "sha-notes",
|
|
481
|
-
content: Buffer.from("# Notes").toString("base64"),
|
|
482
|
-
encoding: "base64",
|
|
483
|
-
},
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
const err: any = new Error("Not Found");
|
|
487
|
-
err.status = 404;
|
|
488
|
-
throw err;
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
(adapter as any).client.rest.issues.listForRepo = async (
|
|
492
|
-
_params: any,
|
|
493
|
-
) => ({ data: [] });
|
|
494
|
-
|
|
495
|
-
const docs = await adapter.getDocuments("FP-P6");
|
|
496
|
-
|
|
497
|
-
expect(docs.length).toBe(2);
|
|
498
|
-
const arch = docs.find((d: any) => d.filename === "architecture.md");
|
|
499
|
-
expect(arch).toBeDefined();
|
|
500
|
-
expect(arch.content).toBe("# Architecture");
|
|
501
|
-
expect(arch.url).toBe(
|
|
502
|
-
"https://github.com/test-owner/test-repo/blob/main/prds/fp-p6/architecture.md",
|
|
503
|
-
);
|
|
504
|
-
expect(arch.prdRef).toBe("FP-P6");
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
test("filters to only .md files", async () => {
|
|
508
|
-
const adapter = makeAdapter();
|
|
509
|
-
|
|
510
|
-
(adapter as any).client.rest.repos.getContent = async (params: any) => {
|
|
511
|
-
if (params.path === ".flux/github-index.json") {
|
|
512
|
-
const err: any = new Error("Not Found");
|
|
513
|
-
err.status = 404;
|
|
514
|
-
throw err;
|
|
515
|
-
}
|
|
516
|
-
if (params.path === "prds/fp-p6") {
|
|
517
|
-
return {
|
|
518
|
-
data: [
|
|
519
|
-
{
|
|
520
|
-
type: "file",
|
|
521
|
-
name: "architecture.md",
|
|
522
|
-
path: "prds/fp-p6/architecture.md",
|
|
523
|
-
sha: "sha-arch",
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
type: "file",
|
|
527
|
-
name: "image.png",
|
|
528
|
-
path: "prds/fp-p6/image.png",
|
|
529
|
-
sha: "sha-png",
|
|
530
|
-
},
|
|
531
|
-
{
|
|
532
|
-
type: "dir",
|
|
533
|
-
name: "subdir",
|
|
534
|
-
path: "prds/fp-p6/subdir",
|
|
535
|
-
sha: "sha-dir",
|
|
536
|
-
},
|
|
537
|
-
],
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
if (params.path === "prds/fp-p6/architecture.md") {
|
|
541
|
-
return {
|
|
542
|
-
data: {
|
|
543
|
-
sha: "sha-arch",
|
|
544
|
-
content: Buffer.from("# Architecture").toString("base64"),
|
|
545
|
-
encoding: "base64",
|
|
546
|
-
},
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
const err: any = new Error("Not Found");
|
|
550
|
-
err.status = 404;
|
|
551
|
-
throw err;
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
(adapter as any).client.rest.issues.listForRepo = async (
|
|
555
|
-
_params: any,
|
|
556
|
-
) => ({ data: [] });
|
|
557
|
-
|
|
558
|
-
const docs = await adapter.getDocuments("FP-P6");
|
|
559
|
-
|
|
560
|
-
expect(docs.length).toBe(1);
|
|
561
|
-
expect(docs[0].filename).toBe("architecture.md");
|
|
562
|
-
});
|
|
563
|
-
});
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
describe("GitHubAdapter Stats", () => {
|
|
567
|
-
beforeEach(() => {
|
|
568
|
-
mockIssuesGet = null;
|
|
569
|
-
mockIssuesList = [];
|
|
570
|
-
mockGetContent = null;
|
|
571
|
-
mockGetContentByPath = {};
|
|
572
|
-
mockIssuesUpdate = null;
|
|
573
|
-
lastUpdateParams = null;
|
|
574
|
-
lastPutParams = null;
|
|
575
|
-
lastDeleteParams = null;
|
|
576
|
-
issueNodeIdCounter = 0;
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
afterEach(() => {});
|
|
580
|
-
|
|
581
|
-
function makeStatsAdapter() {
|
|
582
|
-
const { GitHubAdapter } = require("../adapter.js");
|
|
583
|
-
const adapter = new GitHubAdapter({
|
|
584
|
-
token: "ghp_test",
|
|
585
|
-
owner: "test-owner",
|
|
586
|
-
repo: "test-repo",
|
|
587
|
-
projectId: "PVT_kwDO123",
|
|
588
|
-
refPrefix: "FP",
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
return adapter;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
test("getStats returns Stats shape matching BackendAdapter interface", async () => {
|
|
595
|
-
const adapter = makeStatsAdapter();
|
|
596
|
-
|
|
597
|
-
(adapter as any).client.rest.issues.listForRepo = async (_params: any) => ({
|
|
598
|
-
data: [],
|
|
599
|
-
});
|
|
600
|
-
(adapter as any).client.rest.repos.getContent = async (_params: any) => {
|
|
601
|
-
const err: any = new Error("Not Found");
|
|
602
|
-
err.status = 404;
|
|
603
|
-
throw err;
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
const stats = await adapter.getStats();
|
|
607
|
-
|
|
608
|
-
expect(stats).toHaveProperty("prds");
|
|
609
|
-
expect(stats).toHaveProperty("epics");
|
|
610
|
-
expect(stats).toHaveProperty("tasks");
|
|
611
|
-
expect(stats.prds).toHaveProperty("total");
|
|
612
|
-
expect(stats.prds).toHaveProperty("draft");
|
|
613
|
-
expect(stats.prds).toHaveProperty("pendingReview");
|
|
614
|
-
expect(stats.prds).toHaveProperty("reviewed");
|
|
615
|
-
expect(stats.prds).toHaveProperty("approved");
|
|
616
|
-
expect(stats.prds).toHaveProperty("breakdownReady");
|
|
617
|
-
expect(stats.prds).toHaveProperty("completed");
|
|
618
|
-
expect(stats.prds).toHaveProperty("archived");
|
|
619
|
-
expect(stats.epics).toHaveProperty("total");
|
|
620
|
-
expect(stats.epics).toHaveProperty("pending");
|
|
621
|
-
expect(stats.epics).toHaveProperty("inProgress");
|
|
622
|
-
expect(stats.epics).toHaveProperty("completed");
|
|
623
|
-
expect(stats.tasks).toHaveProperty("total");
|
|
624
|
-
expect(stats.tasks).toHaveProperty("pending");
|
|
625
|
-
expect(stats.tasks).toHaveProperty("inProgress");
|
|
626
|
-
expect(stats.tasks).toHaveProperty("completed");
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
test("PRD counts: draft/pendingReview/etc buckets correctly counted", async () => {
|
|
630
|
-
const adapter = makeStatsAdapter();
|
|
631
|
-
|
|
632
|
-
const openPrds = [
|
|
633
|
-
makeIssue({
|
|
634
|
-
number: 1,
|
|
635
|
-
labels: ["flux:prd", "status:draft"],
|
|
636
|
-
state: "open",
|
|
637
|
-
}),
|
|
638
|
-
makeIssue({
|
|
639
|
-
number: 2,
|
|
640
|
-
labels: ["flux:prd", "status:draft"],
|
|
641
|
-
state: "open",
|
|
642
|
-
}),
|
|
643
|
-
makeIssue({
|
|
644
|
-
number: 3,
|
|
645
|
-
labels: ["flux:prd", "status:pending-review"],
|
|
646
|
-
state: "open",
|
|
647
|
-
}),
|
|
648
|
-
makeIssue({
|
|
649
|
-
number: 4,
|
|
650
|
-
labels: ["flux:prd", "status:reviewed"],
|
|
651
|
-
state: "open",
|
|
652
|
-
}),
|
|
653
|
-
makeIssue({
|
|
654
|
-
number: 5,
|
|
655
|
-
labels: ["flux:prd", "status:approved"],
|
|
656
|
-
state: "open",
|
|
657
|
-
}),
|
|
658
|
-
makeIssue({
|
|
659
|
-
number: 6,
|
|
660
|
-
labels: ["flux:prd", "status:breakdown-ready"],
|
|
661
|
-
state: "open",
|
|
662
|
-
}),
|
|
663
|
-
makeIssue({
|
|
664
|
-
number: 7,
|
|
665
|
-
labels: ["flux:prd", "status:completed"],
|
|
666
|
-
state: "open",
|
|
667
|
-
}),
|
|
668
|
-
];
|
|
669
|
-
|
|
670
|
-
(adapter as any).client.rest.issues.listForRepo = async (params: any) => {
|
|
671
|
-
const labels = params.labels ?? "";
|
|
672
|
-
if (labels.includes("flux:prd") && params.state === "open") {
|
|
673
|
-
return { data: openPrds };
|
|
674
|
-
}
|
|
675
|
-
return { data: [] };
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
const stats = await adapter.getStats();
|
|
679
|
-
|
|
680
|
-
expect(stats.prds.draft).toBe(2);
|
|
681
|
-
expect(stats.prds.pendingReview).toBe(1);
|
|
682
|
-
expect(stats.prds.reviewed).toBe(1);
|
|
683
|
-
expect(stats.prds.approved).toBe(1);
|
|
684
|
-
expect(stats.prds.breakdownReady).toBe(1);
|
|
685
|
-
expect(stats.prds.completed).toBe(1);
|
|
686
|
-
expect(stats.prds.total).toBe(7);
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
test("archived = closed PRD issues; total excludes archived", async () => {
|
|
690
|
-
const adapter = makeStatsAdapter();
|
|
691
|
-
|
|
692
|
-
const openPrds = [
|
|
693
|
-
makeIssue({
|
|
694
|
-
number: 1,
|
|
695
|
-
labels: ["flux:prd", "status:draft"],
|
|
696
|
-
state: "open",
|
|
697
|
-
}),
|
|
698
|
-
makeIssue({
|
|
699
|
-
number: 2,
|
|
700
|
-
labels: ["flux:prd", "status:approved"],
|
|
701
|
-
state: "open",
|
|
702
|
-
}),
|
|
703
|
-
];
|
|
704
|
-
const closedPrds = [
|
|
705
|
-
makeIssue({ number: 3, labels: ["flux:prd"], state: "closed" }),
|
|
706
|
-
makeIssue({ number: 4, labels: ["flux:prd"], state: "closed" }),
|
|
707
|
-
makeIssue({ number: 5, labels: ["flux:prd"], state: "closed" }),
|
|
708
|
-
];
|
|
709
|
-
|
|
710
|
-
(adapter as any).client.rest.issues.listForRepo = async (params: any) => {
|
|
711
|
-
const labels = params.labels ?? "";
|
|
712
|
-
if (!labels.includes("flux:prd")) return { data: [] };
|
|
713
|
-
if (params.state === "open") return { data: openPrds };
|
|
714
|
-
if (params.state === "closed") return { data: closedPrds };
|
|
715
|
-
return { data: [] };
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
const stats = await adapter.getStats();
|
|
719
|
-
|
|
720
|
-
expect(stats.prds.total).toBe(2);
|
|
721
|
-
expect(stats.prds.archived).toBe(3);
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
test("epic and task counts by open/closed state", async () => {
|
|
725
|
-
const adapter = makeStatsAdapter();
|
|
726
|
-
|
|
727
|
-
const openEpics = [
|
|
728
|
-
makeIssue({
|
|
729
|
-
number: 10,
|
|
730
|
-
labels: ["flux:epic", "status:pending"],
|
|
731
|
-
state: "open",
|
|
732
|
-
}),
|
|
733
|
-
makeIssue({
|
|
734
|
-
number: 11,
|
|
735
|
-
labels: ["flux:epic", "status:in-progress"],
|
|
736
|
-
state: "open",
|
|
737
|
-
}),
|
|
738
|
-
];
|
|
739
|
-
const closedEpics = [
|
|
740
|
-
makeIssue({ number: 12, labels: ["flux:epic"], state: "closed" }),
|
|
741
|
-
];
|
|
742
|
-
const openTasks = [
|
|
743
|
-
makeIssue({
|
|
744
|
-
number: 20,
|
|
745
|
-
labels: ["flux:task", "status:pending"],
|
|
746
|
-
state: "open",
|
|
747
|
-
}),
|
|
748
|
-
makeIssue({
|
|
749
|
-
number: 21,
|
|
750
|
-
labels: ["flux:task", "status:pending"],
|
|
751
|
-
state: "open",
|
|
752
|
-
}),
|
|
753
|
-
makeIssue({
|
|
754
|
-
number: 22,
|
|
755
|
-
labels: ["flux:task", "status:in-progress"],
|
|
756
|
-
state: "open",
|
|
757
|
-
}),
|
|
758
|
-
];
|
|
759
|
-
const closedTasks = [
|
|
760
|
-
makeIssue({ number: 23, labels: ["flux:task"], state: "closed" }),
|
|
761
|
-
makeIssue({ number: 24, labels: ["flux:task"], state: "closed" }),
|
|
762
|
-
];
|
|
763
|
-
|
|
764
|
-
(adapter as any).client.rest.issues.listForRepo = async (params: any) => {
|
|
765
|
-
const labels = params.labels ?? "";
|
|
766
|
-
if (labels.includes("flux:prd")) return { data: [] };
|
|
767
|
-
if (labels.includes("flux:epic") && params.state === "open")
|
|
768
|
-
return { data: openEpics };
|
|
769
|
-
if (labels.includes("flux:epic") && params.state === "closed")
|
|
770
|
-
return { data: closedEpics };
|
|
771
|
-
if (labels.includes("flux:task") && params.state === "open")
|
|
772
|
-
return { data: openTasks };
|
|
773
|
-
if (labels.includes("flux:task") && params.state === "closed")
|
|
774
|
-
return { data: closedTasks };
|
|
775
|
-
return { data: [] };
|
|
776
|
-
};
|
|
777
|
-
|
|
778
|
-
const stats = await adapter.getStats();
|
|
779
|
-
|
|
780
|
-
expect(stats.epics.total).toBe(3);
|
|
781
|
-
expect(stats.epics.pending).toBe(1);
|
|
782
|
-
expect(stats.epics.inProgress).toBe(1);
|
|
783
|
-
expect(stats.epics.completed).toBe(1);
|
|
784
|
-
expect(stats.tasks.total).toBe(5);
|
|
785
|
-
expect(stats.tasks.pending).toBe(2);
|
|
786
|
-
expect(stats.tasks.inProgress).toBe(1);
|
|
787
|
-
expect(stats.tasks.completed).toBe(2);
|
|
788
|
-
});
|
|
789
|
-
});
|