@cliangdev/flux-plugin 0.3.0 → 0.3.1-dev.bdbaeae
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/bin/install.cjs +2 -2
- package/commands/dashboard.md +1 -1
- package/package.json +3 -1
- package/src/server/adapters/factory.ts +6 -28
- package/src/server/adapters/github/__tests__/criteria-deps.test.ts +579 -0
- package/src/server/adapters/github/__tests__/documents-stats.test.ts +789 -0
- package/src/server/adapters/github/__tests__/epic-task-crud.test.ts +1072 -0
- package/src/server/adapters/github/__tests__/foundation.test.ts +537 -0
- package/src/server/adapters/github/__tests__/index-store.test.ts +319 -0
- package/src/server/adapters/github/__tests__/prd-crud.test.ts +836 -0
- package/src/server/adapters/github/adapter.ts +1552 -0
- package/src/server/adapters/github/client.ts +33 -0
- package/src/server/adapters/github/config.ts +59 -0
- package/src/server/adapters/github/helpers/criteria.ts +157 -0
- package/src/server/adapters/github/helpers/index-store.ts +75 -0
- package/src/server/adapters/github/helpers/meta.ts +26 -0
- package/src/server/adapters/github/index.ts +5 -0
- package/src/server/adapters/github/mappers/epic.ts +21 -0
- package/src/server/adapters/github/mappers/index.ts +15 -0
- package/src/server/adapters/github/mappers/prd.ts +50 -0
- package/src/server/adapters/github/mappers/task.ts +37 -0
- package/src/server/adapters/github/types.ts +27 -0
- package/src/server/adapters/types.ts +1 -1
- package/src/server/index.ts +2 -0
- package/src/server/tools/__tests__/mcp-interface.test.ts +6 -0
- package/src/server/tools/__tests__/z-configure-github.test.ts +509 -0
- package/src/server/tools/__tests__/z-init-project.test.ts +168 -0
- package/src/server/tools/configure-github.ts +411 -0
- package/src/server/tools/index.ts +2 -1
- package/src/server/tools/init-project.ts +26 -12
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
const TEST_DIR = `/tmp/flux-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
12
|
+
const FLUX_DIR = join(TEST_DIR, ".flux");
|
|
13
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
14
|
+
|
|
15
|
+
let mockTokenValid = true;
|
|
16
|
+
let mockRepoExists = false;
|
|
17
|
+
let mockRemoteConfigExists = false;
|
|
18
|
+
let mockRemoteConfig: Record<string, unknown> | null = null;
|
|
19
|
+
let mockExistingLabels: string[] = [];
|
|
20
|
+
let mockProjectId = "PVT_kwDOtest123";
|
|
21
|
+
let mockProjectUrl = "https://github.com/orgs/testowner/projects/1";
|
|
22
|
+
let mockOwnerId = "U_kgDOtest123";
|
|
23
|
+
let createdLabels: string[] = [];
|
|
24
|
+
let repoCreated = false;
|
|
25
|
+
let configCommitted = false;
|
|
26
|
+
|
|
27
|
+
mock.module("@octokit/rest", () => ({
|
|
28
|
+
Octokit: class MockOctokit {
|
|
29
|
+
constructor(_opts: { auth: string }) {}
|
|
30
|
+
|
|
31
|
+
users = {
|
|
32
|
+
getAuthenticated: async () => {
|
|
33
|
+
if (!mockTokenValid) {
|
|
34
|
+
const err: any = new Error("Bad credentials");
|
|
35
|
+
err.status = 401;
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
return { data: { login: "testowner", id: 123456 } };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
repos = {
|
|
43
|
+
get: async (_opts: { owner: string; repo: string }) => {
|
|
44
|
+
if (!mockRepoExists) {
|
|
45
|
+
const err: any = new Error("Not Found");
|
|
46
|
+
err.status = 404;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
data: { html_url: `https://github.com/${_opts.owner}/${_opts.repo}` },
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
createForAuthenticatedUser: async (opts: {
|
|
54
|
+
name: string;
|
|
55
|
+
private: boolean;
|
|
56
|
+
auto_init: boolean;
|
|
57
|
+
}) => {
|
|
58
|
+
repoCreated = true;
|
|
59
|
+
return {
|
|
60
|
+
data: { html_url: `https://github.com/testowner/${opts.name}` },
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
issues = {
|
|
66
|
+
listLabelsForRepo: async (_opts: { owner: string; repo: string }) => {
|
|
67
|
+
return {
|
|
68
|
+
data: mockExistingLabels.map((name) => ({ name, color: "000000" })),
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
createLabel: async (opts: {
|
|
72
|
+
owner: string;
|
|
73
|
+
repo: string;
|
|
74
|
+
name: string;
|
|
75
|
+
color: string;
|
|
76
|
+
}) => {
|
|
77
|
+
createdLabels.push(opts.name);
|
|
78
|
+
return { data: { name: opts.name, color: opts.color } };
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
repos_getContent = {};
|
|
83
|
+
|
|
84
|
+
request = async (route: string, opts: any) => {
|
|
85
|
+
if (route === "GET /repos/{owner}/{repo}/contents/{path}") {
|
|
86
|
+
if (!mockRemoteConfigExists) {
|
|
87
|
+
const err: any = new Error("Not Found");
|
|
88
|
+
err.status = 404;
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
const content = Buffer.from(JSON.stringify(mockRemoteConfig)).toString(
|
|
92
|
+
"base64",
|
|
93
|
+
);
|
|
94
|
+
return { data: { content, encoding: "base64" } };
|
|
95
|
+
}
|
|
96
|
+
if (route === "PUT /repos/{owner}/{repo}/contents/{path}") {
|
|
97
|
+
configCommitted = true;
|
|
98
|
+
return { data: { content: { sha: "abc123" } } };
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Unhandled route: ${route}`);
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
mock.module("@octokit/graphql", () => ({
|
|
106
|
+
graphql: Object.assign(
|
|
107
|
+
async (_query: string, _vars: any) => {
|
|
108
|
+
throw new Error("Use graphql.defaults instead");
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
defaults: (_opts: any) => {
|
|
112
|
+
return async (query: string, vars: any) => {
|
|
113
|
+
if (query.includes("GetOwnerId")) {
|
|
114
|
+
return { user: { id: mockOwnerId } };
|
|
115
|
+
}
|
|
116
|
+
if (query.includes("CreateProject")) {
|
|
117
|
+
return {
|
|
118
|
+
createProjectV2: {
|
|
119
|
+
projectV2: { id: mockProjectId, url: mockProjectUrl },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Unhandled GraphQL query: ${query}`);
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
import { clearAdapterCache } from "../../adapters/index.js";
|
|
131
|
+
import { config } from "../../config.js";
|
|
132
|
+
import { configureGithubTool } from "../configure-github.js";
|
|
133
|
+
|
|
134
|
+
describe("configure_github MCP Tool", () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
config.clearCache();
|
|
137
|
+
clearAdapterCache();
|
|
138
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
139
|
+
|
|
140
|
+
mockTokenValid = true;
|
|
141
|
+
mockRepoExists = false;
|
|
142
|
+
mockRemoteConfigExists = false;
|
|
143
|
+
mockRemoteConfig = null;
|
|
144
|
+
mockExistingLabels = [];
|
|
145
|
+
mockProjectId = "PVT_kwDOtest123";
|
|
146
|
+
mockProjectUrl = "https://github.com/orgs/testowner/projects/1";
|
|
147
|
+
mockOwnerId = "U_kgDOtest123";
|
|
148
|
+
createdLabels = [];
|
|
149
|
+
repoCreated = false;
|
|
150
|
+
configCommitted = false;
|
|
151
|
+
|
|
152
|
+
mkdirSync(FLUX_DIR, { recursive: true });
|
|
153
|
+
|
|
154
|
+
const projectJsonPath = join(FLUX_DIR, "project.json");
|
|
155
|
+
const initialProject = {
|
|
156
|
+
name: "test-project",
|
|
157
|
+
ref_prefix: "TEST",
|
|
158
|
+
adapter: { type: "local" },
|
|
159
|
+
};
|
|
160
|
+
writeFileSync(projectJsonPath, JSON.stringify(initialProject, null, 2));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
afterEach(() => {
|
|
164
|
+
clearAdapterCache();
|
|
165
|
+
config.clearCache();
|
|
166
|
+
if (existsSync(TEST_DIR)) {
|
|
167
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("input validation", () => {
|
|
172
|
+
test("rejects missing token", async () => {
|
|
173
|
+
await expect(
|
|
174
|
+
configureGithubTool.handler({
|
|
175
|
+
owner: "testowner",
|
|
176
|
+
repo: "flux-tracking",
|
|
177
|
+
}),
|
|
178
|
+
).rejects.toThrow();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("rejects missing owner", async () => {
|
|
182
|
+
await expect(
|
|
183
|
+
configureGithubTool.handler({
|
|
184
|
+
token: "ghp_test123",
|
|
185
|
+
repo: "flux-tracking",
|
|
186
|
+
}),
|
|
187
|
+
).rejects.toThrow();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("rejects missing repo", async () => {
|
|
191
|
+
await expect(
|
|
192
|
+
configureGithubTool.handler({
|
|
193
|
+
token: "ghp_test123",
|
|
194
|
+
owner: "testowner",
|
|
195
|
+
}),
|
|
196
|
+
).rejects.toThrow();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("visibility defaults to private when omitted", async () => {
|
|
200
|
+
const result = (await configureGithubTool.handler({
|
|
201
|
+
token: "ghp_test123",
|
|
202
|
+
owner: "testowner",
|
|
203
|
+
repo: "flux-tracking",
|
|
204
|
+
})) as any;
|
|
205
|
+
|
|
206
|
+
expect(result.mode).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("token validation", () => {
|
|
211
|
+
test("returns descriptive error when PAT validation fails", async () => {
|
|
212
|
+
mockTokenValid = false;
|
|
213
|
+
|
|
214
|
+
await expect(
|
|
215
|
+
configureGithubTool.handler({
|
|
216
|
+
token: "ghp_invalid",
|
|
217
|
+
owner: "testowner",
|
|
218
|
+
repo: "flux-tracking",
|
|
219
|
+
}),
|
|
220
|
+
).rejects.toThrow(/token|credentials|auth/i);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("mode detection", () => {
|
|
225
|
+
test("returns mode: setup when neither local config nor remote config exists", async () => {
|
|
226
|
+
const result = (await configureGithubTool.handler({
|
|
227
|
+
token: "ghp_test123",
|
|
228
|
+
owner: "testowner",
|
|
229
|
+
repo: "flux-tracking",
|
|
230
|
+
})) as any;
|
|
231
|
+
|
|
232
|
+
expect(result.mode).toBe("setup");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("returns mode: join when remote .flux/github-config.json is readable", async () => {
|
|
236
|
+
mockRemoteConfigExists = true;
|
|
237
|
+
mockRemoteConfig = {
|
|
238
|
+
owner: "testowner",
|
|
239
|
+
repo: "flux-tracking",
|
|
240
|
+
projectId: "PVT_kwDOremote123",
|
|
241
|
+
refPrefix: "FLUX",
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const result = (await configureGithubTool.handler({
|
|
245
|
+
token: "ghp_test123",
|
|
246
|
+
owner: "testowner",
|
|
247
|
+
repo: "flux-tracking",
|
|
248
|
+
})) as any;
|
|
249
|
+
|
|
250
|
+
expect(result.mode).toBe("join");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("returns mode: update when local project.json already has github adapter config", async () => {
|
|
254
|
+
const projectJsonPath = join(FLUX_DIR, "project.json");
|
|
255
|
+
const existingProject = {
|
|
256
|
+
name: "test-project",
|
|
257
|
+
ref_prefix: "FLUX",
|
|
258
|
+
adapter: {
|
|
259
|
+
type: "github",
|
|
260
|
+
config: {
|
|
261
|
+
token: "ghp_old_token",
|
|
262
|
+
owner: "testowner",
|
|
263
|
+
repo: "flux-tracking",
|
|
264
|
+
projectId: "PVT_kwDOtest123",
|
|
265
|
+
refPrefix: "FLUX",
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
writeFileSync(projectJsonPath, JSON.stringify(existingProject, null, 2));
|
|
270
|
+
|
|
271
|
+
const result = (await configureGithubTool.handler({
|
|
272
|
+
token: "ghp_new_token",
|
|
273
|
+
owner: "testowner",
|
|
274
|
+
repo: "flux-tracking",
|
|
275
|
+
})) as any;
|
|
276
|
+
|
|
277
|
+
expect(result.mode).toBe("update");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("returns mode: setup when project.json has adapter type github but no config (partial init)", async () => {
|
|
281
|
+
const projectJsonPath = join(FLUX_DIR, "project.json");
|
|
282
|
+
const partialProject = {
|
|
283
|
+
name: "test-project",
|
|
284
|
+
ref_prefix: "FLUX",
|
|
285
|
+
adapter: { type: "github" },
|
|
286
|
+
};
|
|
287
|
+
writeFileSync(projectJsonPath, JSON.stringify(partialProject, null, 2));
|
|
288
|
+
|
|
289
|
+
const result = (await configureGithubTool.handler({
|
|
290
|
+
token: "ghp_test123",
|
|
291
|
+
owner: "testowner",
|
|
292
|
+
repo: "flux-tracking",
|
|
293
|
+
})) as any;
|
|
294
|
+
|
|
295
|
+
expect(result.mode).toBe("setup");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("setup mode", () => {
|
|
300
|
+
test("skips repo creation if repo already exists", async () => {
|
|
301
|
+
mockRepoExists = true;
|
|
302
|
+
|
|
303
|
+
const result = (await configureGithubTool.handler({
|
|
304
|
+
token: "ghp_test123",
|
|
305
|
+
owner: "testowner",
|
|
306
|
+
repo: "flux-tracking",
|
|
307
|
+
})) as any;
|
|
308
|
+
|
|
309
|
+
expect(result.mode).toBe("setup");
|
|
310
|
+
expect(repoCreated).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("creates repo when it does not exist", async () => {
|
|
314
|
+
mockRepoExists = false;
|
|
315
|
+
|
|
316
|
+
await configureGithubTool.handler({
|
|
317
|
+
token: "ghp_test123",
|
|
318
|
+
owner: "testowner",
|
|
319
|
+
repo: "flux-tracking",
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(repoCreated).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("skips label creation for labels that already exist", async () => {
|
|
326
|
+
mockExistingLabels = ["flux:prd", "flux:epic", "flux:task"];
|
|
327
|
+
|
|
328
|
+
await configureGithubTool.handler({
|
|
329
|
+
token: "ghp_test123",
|
|
330
|
+
owner: "testowner",
|
|
331
|
+
repo: "flux-tracking",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(createdLabels).not.toContain("flux:prd");
|
|
335
|
+
expect(createdLabels).not.toContain("flux:epic");
|
|
336
|
+
expect(createdLabels).not.toContain("flux:task");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("creates only missing labels", async () => {
|
|
340
|
+
mockExistingLabels = ["flux:prd"];
|
|
341
|
+
mockRepoExists = true;
|
|
342
|
+
|
|
343
|
+
await configureGithubTool.handler({
|
|
344
|
+
token: "ghp_test123",
|
|
345
|
+
owner: "testowner",
|
|
346
|
+
repo: "flux-tracking",
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(createdLabels).not.toContain("flux:prd");
|
|
350
|
+
expect(createdLabels).toContain("flux:epic");
|
|
351
|
+
expect(createdLabels).toContain("flux:task");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("commits .flux/github-config.json with required fields", async () => {
|
|
355
|
+
await configureGithubTool.handler({
|
|
356
|
+
token: "ghp_test123",
|
|
357
|
+
owner: "testowner",
|
|
358
|
+
repo: "flux-tracking",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(configCommitted).toBe(true);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("returns board_url, repo_url, labels_created, mode: setup", async () => {
|
|
365
|
+
const result = (await configureGithubTool.handler({
|
|
366
|
+
token: "ghp_test123",
|
|
367
|
+
owner: "testowner",
|
|
368
|
+
repo: "flux-tracking",
|
|
369
|
+
})) as any;
|
|
370
|
+
|
|
371
|
+
expect(result.mode).toBe("setup");
|
|
372
|
+
expect(result.board_url).toBeDefined();
|
|
373
|
+
expect(result.repo_url).toBeDefined();
|
|
374
|
+
expect(typeof result.labels_created).toBe("number");
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe("join mode", () => {
|
|
379
|
+
beforeEach(() => {
|
|
380
|
+
mockRemoteConfigExists = true;
|
|
381
|
+
mockRemoteConfig = {
|
|
382
|
+
owner: "testowner",
|
|
383
|
+
repo: "flux-tracking",
|
|
384
|
+
projectId: "PVT_kwDOremote123",
|
|
385
|
+
refPrefix: "FLUX",
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("reads remote config and writes correct local project.json", async () => {
|
|
390
|
+
const result = (await configureGithubTool.handler({
|
|
391
|
+
token: "ghp_test123",
|
|
392
|
+
owner: "testowner",
|
|
393
|
+
repo: "flux-tracking",
|
|
394
|
+
})) as any;
|
|
395
|
+
|
|
396
|
+
expect(result.mode).toBe("join");
|
|
397
|
+
|
|
398
|
+
const projectJsonPath = join(FLUX_DIR, "project.json");
|
|
399
|
+
const saved = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
400
|
+
expect(saved.adapter.type).toBe("github");
|
|
401
|
+
expect(saved.adapter.config.token).toBe("ghp_test123");
|
|
402
|
+
expect(saved.adapter.config.projectId).toBe("PVT_kwDOremote123");
|
|
403
|
+
expect(saved.adapter.config.refPrefix).toBe("FLUX");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("does not trigger label or board creation in join mode", async () => {
|
|
407
|
+
await configureGithubTool.handler({
|
|
408
|
+
token: "ghp_test123",
|
|
409
|
+
owner: "testowner",
|
|
410
|
+
repo: "flux-tracking",
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(createdLabels).toHaveLength(0);
|
|
414
|
+
expect(configCommitted).toBe(false);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("returns mode: join with labels_created: 0", async () => {
|
|
418
|
+
const result = (await configureGithubTool.handler({
|
|
419
|
+
token: "ghp_test123",
|
|
420
|
+
owner: "testowner",
|
|
421
|
+
repo: "flux-tracking",
|
|
422
|
+
})) as any;
|
|
423
|
+
|
|
424
|
+
expect(result.mode).toBe("join");
|
|
425
|
+
expect(result.labels_created).toBe(0);
|
|
426
|
+
expect(result.repo_url).toBeDefined();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("returns error with collaborator instructions when remote config is 404", async () => {
|
|
430
|
+
mockRemoteConfigExists = false;
|
|
431
|
+
mockRemoteConfig = null;
|
|
432
|
+
|
|
433
|
+
const result = (await configureGithubTool.handler({
|
|
434
|
+
token: "ghp_test123",
|
|
435
|
+
owner: "testowner",
|
|
436
|
+
repo: "flux-tracking",
|
|
437
|
+
})) as any;
|
|
438
|
+
|
|
439
|
+
expect(result.mode).toBe("setup");
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("update mode (token rotation)", () => {
|
|
444
|
+
beforeEach(() => {
|
|
445
|
+
const projectJsonPath = join(FLUX_DIR, "project.json");
|
|
446
|
+
const existingProject = {
|
|
447
|
+
name: "test-project",
|
|
448
|
+
ref_prefix: "FLUX",
|
|
449
|
+
adapter: {
|
|
450
|
+
type: "github",
|
|
451
|
+
config: {
|
|
452
|
+
token: "ghp_old_token",
|
|
453
|
+
owner: "testowner",
|
|
454
|
+
repo: "flux-tracking",
|
|
455
|
+
projectId: "PVT_kwDOtest123",
|
|
456
|
+
refPrefix: "FLUX",
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
writeFileSync(projectJsonPath, JSON.stringify(existingProject, null, 2));
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("updates only the token field, leaves other fields unchanged", async () => {
|
|
464
|
+
const result = (await configureGithubTool.handler({
|
|
465
|
+
token: "ghp_new_token",
|
|
466
|
+
owner: "testowner",
|
|
467
|
+
repo: "flux-tracking",
|
|
468
|
+
})) as any;
|
|
469
|
+
|
|
470
|
+
expect(result.mode).toBe("update");
|
|
471
|
+
|
|
472
|
+
const projectJsonPath = join(FLUX_DIR, "project.json");
|
|
473
|
+
const saved = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
474
|
+
expect(saved.adapter.config.token).toBe("ghp_new_token");
|
|
475
|
+
expect(saved.adapter.config.projectId).toBe("PVT_kwDOtest123");
|
|
476
|
+
expect(saved.adapter.config.refPrefix).toBe("FLUX");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("returns mode: update with labels_created: 0", async () => {
|
|
480
|
+
const result = (await configureGithubTool.handler({
|
|
481
|
+
token: "ghp_new_token",
|
|
482
|
+
owner: "testowner",
|
|
483
|
+
repo: "flux-tracking",
|
|
484
|
+
})) as any;
|
|
485
|
+
|
|
486
|
+
expect(result.mode).toBe("update");
|
|
487
|
+
expect(result.labels_created).toBe(0);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe("response shape", () => {
|
|
492
|
+
test("contains board_url, repo_url, labels_created, mode in setup", async () => {
|
|
493
|
+
const result = (await configureGithubTool.handler({
|
|
494
|
+
token: "ghp_test123",
|
|
495
|
+
owner: "testowner",
|
|
496
|
+
repo: "flux-tracking",
|
|
497
|
+
})) as any;
|
|
498
|
+
|
|
499
|
+
expect(result).toHaveProperty("board_url");
|
|
500
|
+
expect(result).toHaveProperty("repo_url");
|
|
501
|
+
expect(result).toHaveProperty("labels_created");
|
|
502
|
+
expect(result).toHaveProperty("mode");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("tool appears with correct name", () => {
|
|
506
|
+
expect(configureGithubTool.name).toBe("configure_github");
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const TEST_DIR = `/tmp/flux-init-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
6
|
+
const FLUX_DIR = join(TEST_DIR, ".flux");
|
|
7
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
8
|
+
|
|
9
|
+
import { config } from "../../config.js";
|
|
10
|
+
import { closeDb } from "../../db/index.js";
|
|
11
|
+
import { initProjectTool } from "../init-project.js";
|
|
12
|
+
|
|
13
|
+
describe("init_project", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
closeDb();
|
|
16
|
+
config.clearCache();
|
|
17
|
+
if (existsSync(FLUX_DIR)) {
|
|
18
|
+
rmSync(FLUX_DIR, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
closeDb();
|
|
25
|
+
config.clearCache();
|
|
26
|
+
if (existsSync(TEST_DIR)) {
|
|
27
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("local adapter", () => {
|
|
32
|
+
test("creates .flux directory and project.json", async () => {
|
|
33
|
+
await initProjectTool.handler({
|
|
34
|
+
name: "Test Project",
|
|
35
|
+
vision: "Test vision",
|
|
36
|
+
adapter: "local",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(existsSync(FLUX_DIR)).toBe(true);
|
|
40
|
+
expect(existsSync(join(FLUX_DIR, "project.json"))).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("initializes SQLite database for local adapter", async () => {
|
|
44
|
+
await initProjectTool.handler({
|
|
45
|
+
name: "Test Project",
|
|
46
|
+
vision: "Test vision",
|
|
47
|
+
adapter: "local",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(existsSync(join(FLUX_DIR, "flux.db"))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("returns planning message for local adapter", async () => {
|
|
54
|
+
const result = (await initProjectTool.handler({
|
|
55
|
+
name: "Test Project",
|
|
56
|
+
vision: "Test vision",
|
|
57
|
+
})) as any;
|
|
58
|
+
|
|
59
|
+
expect(result.success).toBe(true);
|
|
60
|
+
expect(result.message).toContain("/flux:prd");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("github adapter", () => {
|
|
65
|
+
test("creates .flux directory and project.json", async () => {
|
|
66
|
+
await initProjectTool.handler({
|
|
67
|
+
name: "Test Project",
|
|
68
|
+
vision: "Test vision",
|
|
69
|
+
adapter: "github",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(existsSync(FLUX_DIR)).toBe(true);
|
|
73
|
+
expect(existsSync(join(FLUX_DIR, "project.json"))).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("does not create SQLite database for github adapter", async () => {
|
|
77
|
+
await initProjectTool.handler({
|
|
78
|
+
name: "Test Project",
|
|
79
|
+
vision: "Test vision",
|
|
80
|
+
adapter: "github",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(existsSync(join(FLUX_DIR, "flux.db"))).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("project.json records adapter type as github", async () => {
|
|
87
|
+
const result = (await initProjectTool.handler({
|
|
88
|
+
name: "Test Project",
|
|
89
|
+
vision: "Test vision",
|
|
90
|
+
adapter: "github",
|
|
91
|
+
})) as any;
|
|
92
|
+
|
|
93
|
+
expect(result.project.adapter.type).toBe("github");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("returns configure_github next-step message", async () => {
|
|
97
|
+
const result = (await initProjectTool.handler({
|
|
98
|
+
name: "Test Project",
|
|
99
|
+
vision: "Test vision",
|
|
100
|
+
adapter: "github",
|
|
101
|
+
})) as any;
|
|
102
|
+
|
|
103
|
+
expect(result.success).toBe(true);
|
|
104
|
+
expect(result.message).toContain("configure_github");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("fails if project already initialized", async () => {
|
|
108
|
+
await initProjectTool.handler({
|
|
109
|
+
name: "Test Project",
|
|
110
|
+
vision: "Test vision",
|
|
111
|
+
adapter: "github",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await expect(
|
|
115
|
+
initProjectTool.handler({
|
|
116
|
+
name: "Test Project",
|
|
117
|
+
vision: "Test vision",
|
|
118
|
+
adapter: "github",
|
|
119
|
+
}),
|
|
120
|
+
).rejects.toThrow(/already initialized/i);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("linear adapter", () => {
|
|
125
|
+
test("does not create SQLite database for linear adapter", async () => {
|
|
126
|
+
await initProjectTool.handler({
|
|
127
|
+
name: "Test Project",
|
|
128
|
+
vision: "Test vision",
|
|
129
|
+
adapter: "linear",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(existsSync(join(FLUX_DIR, "flux.db"))).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("returns configure_linear next-step message", async () => {
|
|
136
|
+
const result = (await initProjectTool.handler({
|
|
137
|
+
name: "Test Project",
|
|
138
|
+
vision: "Test vision",
|
|
139
|
+
adapter: "linear",
|
|
140
|
+
})) as any;
|
|
141
|
+
|
|
142
|
+
expect(result.message).toContain("configure_linear");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("input validation", () => {
|
|
147
|
+
test("rejects missing name", async () => {
|
|
148
|
+
await expect(
|
|
149
|
+
initProjectTool.handler({ vision: "Test vision" }),
|
|
150
|
+
).rejects.toThrow();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("rejects missing vision", async () => {
|
|
154
|
+
await expect(
|
|
155
|
+
initProjectTool.handler({ name: "Test Project" }),
|
|
156
|
+
).rejects.toThrow();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("defaults to local adapter when not specified", async () => {
|
|
160
|
+
const result = (await initProjectTool.handler({
|
|
161
|
+
name: "Test Project",
|
|
162
|
+
vision: "Test vision",
|
|
163
|
+
})) as any;
|
|
164
|
+
|
|
165
|
+
expect(result.project.adapter.type).toBe("local");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|