@cliangdev/flux-plugin 0.3.1 → 0.4.0-dev.0892a21

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.
Files changed (32) hide show
  1. package/commands/dashboard.md +1 -1
  2. package/package.json +3 -1
  3. package/src/server/adapters/factory.ts +6 -28
  4. package/src/server/adapters/github/__tests__/criteria-deps.test.ts +579 -0
  5. package/src/server/adapters/github/__tests__/documents-stats.test.ts +789 -0
  6. package/src/server/adapters/github/__tests__/epic-task-crud.test.ts +1072 -0
  7. package/src/server/adapters/github/__tests__/foundation.test.ts +537 -0
  8. package/src/server/adapters/github/__tests__/index-store.test.ts +319 -0
  9. package/src/server/adapters/github/__tests__/prd-crud.test.ts +836 -0
  10. package/src/server/adapters/github/adapter.ts +1574 -0
  11. package/src/server/adapters/github/client.ts +34 -0
  12. package/src/server/adapters/github/config.ts +59 -0
  13. package/src/server/adapters/github/helpers/criteria.ts +157 -0
  14. package/src/server/adapters/github/helpers/index-store.ts +79 -0
  15. package/src/server/adapters/github/helpers/meta.ts +26 -0
  16. package/src/server/adapters/github/index.ts +5 -0
  17. package/src/server/adapters/github/mappers/epic.ts +21 -0
  18. package/src/server/adapters/github/mappers/index.ts +15 -0
  19. package/src/server/adapters/github/mappers/prd.ts +50 -0
  20. package/src/server/adapters/github/mappers/task.ts +37 -0
  21. package/src/server/adapters/github/types.ts +27 -0
  22. package/src/server/adapters/linear/adapter.ts +121 -105
  23. package/src/server/adapters/linear/client.ts +21 -14
  24. package/src/server/adapters/types.ts +1 -1
  25. package/src/server/index.ts +2 -0
  26. package/src/server/tools/__tests__/mcp-interface.test.ts +6 -0
  27. package/src/server/tools/__tests__/z-configure-github.test.ts +521 -0
  28. package/src/server/tools/__tests__/z-get-linear-url.test.ts +2 -2
  29. package/src/server/tools/__tests__/z-init-project.test.ts +168 -0
  30. package/src/server/tools/configure-github.ts +445 -0
  31. package/src/server/tools/index.ts +2 -1
  32. package/src/server/tools/init-project.ts +26 -12
@@ -0,0 +1,319 @@
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-github-index-test-${Date.now()}`;
6
+ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
7
+
8
+ let mockGetContentResponse: any = null;
9
+ let lastPutParams: any = null;
10
+ let mockPutShouldConflict = false;
11
+
12
+ mock.module("@octokit/rest", () => ({
13
+ Octokit: class MockOctokit {
14
+ repos = {
15
+ getContent: async (_params: any) => {
16
+ if (mockGetContentResponse === null) {
17
+ const err: any = new Error("Not Found");
18
+ err.status = 404;
19
+ throw err;
20
+ }
21
+ return mockGetContentResponse;
22
+ },
23
+ createOrUpdateFileContents: async (params: any) => {
24
+ lastPutParams = params;
25
+ if (mockPutShouldConflict) {
26
+ const err: any = new Error("Conflict");
27
+ err.status = 409;
28
+ throw err;
29
+ }
30
+ return { data: { content: { sha: "new-sha-123" } } };
31
+ },
32
+ };
33
+ },
34
+ }));
35
+
36
+ mock.module("@octokit/graphql", () => ({
37
+ graphql: {
38
+ defaults: () => async () => ({}),
39
+ },
40
+ }));
41
+
42
+ describe("index-store", () => {
43
+ beforeEach(() => {
44
+ mockGetContentResponse = null;
45
+ lastPutParams = null;
46
+ mockPutShouldConflict = false;
47
+ });
48
+
49
+ afterEach(() => {});
50
+
51
+ async function makeClient() {
52
+ const { GitHubClient } = await import("../client.js");
53
+ return new GitHubClient({
54
+ token: "ghp_test",
55
+ owner: "test-owner",
56
+ repo: "test-repo",
57
+ projectId: "PVT_123",
58
+ refPrefix: "FP",
59
+ });
60
+ }
61
+
62
+ describe("readIndex", () => {
63
+ test("returns empty object when file does not exist (404)", async () => {
64
+ const { readIndex } = await import("../helpers/index-store.js");
65
+ const client = await makeClient();
66
+
67
+ mockGetContentResponse = null;
68
+
69
+ const result = await readIndex(client, "test-owner", "test-repo");
70
+ expect(result).toEqual({});
71
+ });
72
+
73
+ test("parses and returns existing index", async () => {
74
+ const { readIndex } = await import("../helpers/index-store.js");
75
+ const client = await makeClient();
76
+
77
+ const indexData = { "FP-P1": 1, "FP-E3": 2, "FP-T12": 5 };
78
+ const encoded = Buffer.from(JSON.stringify(indexData)).toString("base64");
79
+ mockGetContentResponse = {
80
+ data: {
81
+ sha: "existing-sha-abc",
82
+ content: encoded,
83
+ encoding: "base64",
84
+ },
85
+ };
86
+
87
+ const result = await readIndex(client, "test-owner", "test-repo");
88
+ expect(result).toEqual({ "FP-P1": 1, "FP-E3": 2, "FP-T12": 5 });
89
+ });
90
+ });
91
+
92
+ describe("writeIndex", () => {
93
+ test("includes SHA in PUT when file exists", async () => {
94
+ const { writeIndex } = await import("../helpers/index-store.js");
95
+ const client = await makeClient();
96
+
97
+ mockGetContentResponse = {
98
+ data: {
99
+ sha: "existing-sha-abc",
100
+ content: Buffer.from("{}").toString("base64"),
101
+ encoding: "base64",
102
+ },
103
+ };
104
+
105
+ await writeIndex(client, "test-owner", "test-repo", { "FP-P1": 42 });
106
+
107
+ expect(lastPutParams.sha).toBe("existing-sha-abc");
108
+ });
109
+
110
+ test("omits SHA when creating new file", async () => {
111
+ const { writeIndex } = await import("../helpers/index-store.js");
112
+ const client = await makeClient();
113
+
114
+ mockGetContentResponse = null;
115
+
116
+ await writeIndex(client, "test-owner", "test-repo", { "FP-P1": 42 });
117
+
118
+ expect(lastPutParams.sha).toBeUndefined();
119
+ });
120
+
121
+ test("throws on 409 conflict", async () => {
122
+ const { writeIndex } = await import("../helpers/index-store.js");
123
+ const client = await makeClient();
124
+
125
+ mockGetContentResponse = {
126
+ data: {
127
+ sha: "existing-sha-abc",
128
+ content: Buffer.from("{}").toString("base64"),
129
+ encoding: "base64",
130
+ },
131
+ };
132
+ mockPutShouldConflict = true;
133
+
134
+ await expect(
135
+ writeIndex(client, "test-owner", "test-repo", { "FP-P1": 42 }),
136
+ ).rejects.toThrow("Index conflict");
137
+ });
138
+
139
+ test("encodes index as base64 JSON in PUT", async () => {
140
+ const { writeIndex } = await import("../helpers/index-store.js");
141
+ const client = await makeClient();
142
+
143
+ mockGetContentResponse = null;
144
+
145
+ await writeIndex(client, "test-owner", "test-repo", { "FP-P1": 42 });
146
+
147
+ const decoded = JSON.parse(
148
+ Buffer.from(lastPutParams.content, "base64").toString("utf-8"),
149
+ );
150
+ expect(decoded).toEqual({ "FP-P1": 42 });
151
+ });
152
+
153
+ test("uses correct commit message", async () => {
154
+ const { writeIndex } = await import("../helpers/index-store.js");
155
+ const client = await makeClient();
156
+
157
+ mockGetContentResponse = null;
158
+
159
+ await writeIndex(client, "test-owner", "test-repo", {});
160
+
161
+ expect(lastPutParams.message).toBe("chore: update flux ref index");
162
+ });
163
+ });
164
+
165
+ describe("round-trip", () => {
166
+ test("write then read returns same data", async () => {
167
+ const { readIndex, writeIndex } = await import(
168
+ "../helpers/index-store.js"
169
+ );
170
+ const client = await makeClient();
171
+
172
+ const indexData = { "FP-P1": 42 };
173
+
174
+ mockGetContentResponse = null;
175
+ await writeIndex(client, "test-owner", "test-repo", indexData);
176
+
177
+ const writtenContent = lastPutParams.content;
178
+ mockGetContentResponse = {
179
+ data: {
180
+ sha: "new-sha-123",
181
+ content: writtenContent,
182
+ encoding: "base64",
183
+ },
184
+ };
185
+
186
+ const result = await readIndex(client, "test-owner", "test-repo");
187
+ expect(result).toEqual({ "FP-P1": 42 });
188
+ });
189
+ });
190
+ });
191
+
192
+ describe("GitHubAdapter index integration", () => {
193
+ let mockIssuesForLabel: any[] = [];
194
+ let mockGetContentForAdapter: any = null;
195
+ let lastAdapterPutParams: any = null;
196
+
197
+ beforeEach(() => {
198
+ mockIssuesForLabel = [];
199
+ mockGetContentForAdapter = null;
200
+ lastAdapterPutParams = null;
201
+ });
202
+
203
+ async function makeAdapter() {
204
+ const { GitHubAdapter } = await import("../adapter.js");
205
+ const adapter = new GitHubAdapter({
206
+ token: "ghp_test",
207
+ owner: "test-owner",
208
+ repo: "test-repo",
209
+ projectId: "PVT_123",
210
+ refPrefix: "FP",
211
+ });
212
+
213
+ (adapter as any).client.rest = {
214
+ repos: {
215
+ getContent: async (_params: any) => {
216
+ if (mockGetContentForAdapter === null) {
217
+ const err: any = new Error("Not Found");
218
+ err.status = 404;
219
+ throw err;
220
+ }
221
+ return mockGetContentForAdapter;
222
+ },
223
+ createOrUpdateFileContents: async (params: any) => {
224
+ lastAdapterPutParams = params;
225
+ return { data: { content: { sha: "new-sha-456" } } };
226
+ },
227
+ },
228
+ issues: {
229
+ listForRepo: async (_params: any) => {
230
+ return { data: mockIssuesForLabel };
231
+ },
232
+ },
233
+ };
234
+
235
+ return adapter;
236
+ }
237
+
238
+ test("resolveRef returns issue number from index when present", async () => {
239
+ const adapter = await makeAdapter();
240
+
241
+ const indexData = { "FP-P1": 7 };
242
+ mockGetContentForAdapter = {
243
+ data: {
244
+ sha: "some-sha",
245
+ content: Buffer.from(JSON.stringify(indexData)).toString("base64"),
246
+ encoding: "base64",
247
+ },
248
+ };
249
+
250
+ const result = await (adapter as any).resolveRef("FP-P1", "flux:prd");
251
+ expect(result).toBe(7);
252
+ });
253
+
254
+ test("resolveRef returns null when ref not in index and no issues found", async () => {
255
+ const adapter = await makeAdapter();
256
+
257
+ mockGetContentForAdapter = null;
258
+ mockIssuesForLabel = [];
259
+
260
+ const result = await (adapter as any).resolveRef("FP-P99", "flux:prd");
261
+ expect(result).toBeNull();
262
+ });
263
+
264
+ test("resolveRef repairs index when found via label search", async () => {
265
+ const adapter = await makeAdapter();
266
+
267
+ mockGetContentForAdapter = null;
268
+
269
+ mockIssuesForLabel = [
270
+ {
271
+ number: 15,
272
+ body: '<!-- flux-meta: {"ref":"FP-E5","type":"epic"} -->',
273
+ labels: [{ name: "flux:epic" }],
274
+ },
275
+ ];
276
+
277
+ const result = await (adapter as any).resolveRef("FP-E5", "flux:epic");
278
+ expect(result).toBe(15);
279
+
280
+ const written = JSON.parse(
281
+ Buffer.from(lastAdapterPutParams.content, "base64").toString("utf-8"),
282
+ );
283
+ expect(written["FP-E5"]).toBe(15);
284
+ });
285
+
286
+ test("addToIndex writes updated index with new ref", async () => {
287
+ const adapter = await makeAdapter();
288
+
289
+ mockGetContentForAdapter = null;
290
+
291
+ await (adapter as any).addToIndex("FP-T10", 22);
292
+
293
+ const written = JSON.parse(
294
+ Buffer.from(lastAdapterPutParams.content, "base64").toString("utf-8"),
295
+ );
296
+ expect(written["FP-T10"]).toBe(22);
297
+ });
298
+
299
+ test("removeFromIndex removes ref from index", async () => {
300
+ const adapter = await makeAdapter();
301
+
302
+ const indexData = { "FP-P1": 1, "FP-E2": 2 };
303
+ mockGetContentForAdapter = {
304
+ data: {
305
+ sha: "some-sha",
306
+ content: Buffer.from(JSON.stringify(indexData)).toString("base64"),
307
+ encoding: "base64",
308
+ },
309
+ };
310
+
311
+ await (adapter as any).removeFromIndex("FP-P1");
312
+
313
+ const written = JSON.parse(
314
+ Buffer.from(lastAdapterPutParams.content, "base64").toString("utf-8"),
315
+ );
316
+ expect(written["FP-P1"]).toBeUndefined();
317
+ expect(written["FP-E2"]).toBe(2);
318
+ });
319
+ });