@cliangdev/flux-plugin 0.2.0 → 0.3.0
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/README.md +11 -7
- package/agents/coder.md +150 -25
- package/bin/install.cjs +171 -16
- package/commands/breakdown.md +47 -10
- package/commands/dashboard.md +29 -0
- package/commands/flux.md +92 -12
- package/commands/implement.md +166 -17
- package/commands/linear.md +6 -5
- package/commands/prd.md +996 -82
- package/manifest.json +2 -1
- package/package.json +9 -11
- package/skills/flux-orchestrator/SKILL.md +11 -3
- package/skills/prd-writer/SKILL.md +761 -0
- package/skills/ux-ui-design/SKILL.md +346 -0
- package/skills/ux-ui-design/references/design-tokens.md +359 -0
- package/src/__tests__/version.test.ts +37 -0
- package/src/adapters/local/.gitkeep +0 -0
- package/src/dashboard/__tests__/api.test.ts +211 -0
- package/src/dashboard/browser.ts +35 -0
- package/src/dashboard/public/app.js +869 -0
- package/src/dashboard/public/index.html +90 -0
- package/src/dashboard/public/styles.css +807 -0
- package/src/dashboard/public/vendor/highlight.css +10 -0
- package/src/dashboard/public/vendor/highlight.min.js +8422 -0
- package/src/dashboard/public/vendor/marked.min.js +2210 -0
- package/src/dashboard/server.ts +296 -0
- package/src/dashboard/watchers.ts +83 -0
- package/src/server/__tests__/config.test.ts +163 -0
- package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
- package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
- package/src/server/adapters/__tests__/dependency-ops.test.ts +429 -0
- package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
- package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
- package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
- package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
- package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
- package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
- package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
- package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
- package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
- package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
- package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
- package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
- package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
- package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
- package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
- package/src/server/adapters/factory.ts +90 -0
- package/src/server/adapters/index.ts +9 -0
- package/src/server/adapters/linear/adapter.ts +1141 -0
- package/src/server/adapters/linear/client.ts +169 -0
- package/src/server/adapters/linear/config.ts +152 -0
- package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
- package/src/server/adapters/linear/helpers/index.ts +7 -0
- package/src/server/adapters/linear/index.ts +16 -0
- package/src/server/adapters/linear/mappers/description.ts +136 -0
- package/src/server/adapters/linear/mappers/epic.ts +81 -0
- package/src/server/adapters/linear/mappers/index.ts +27 -0
- package/src/server/adapters/linear/mappers/prd.ts +178 -0
- package/src/server/adapters/linear/mappers/task.ts +82 -0
- package/src/server/adapters/linear/types.ts +264 -0
- package/src/server/adapters/local-adapter.ts +1009 -0
- package/src/server/adapters/types.ts +293 -0
- package/src/server/config.ts +73 -0
- package/src/server/db/__tests__/queries.test.ts +473 -0
- package/src/server/db/ids.ts +17 -0
- package/src/server/db/index.ts +69 -0
- package/src/server/db/queries.ts +142 -0
- package/src/server/db/refs.ts +60 -0
- package/src/server/db/schema.ts +97 -0
- package/src/server/db/sqlite.ts +10 -0
- package/src/server/index.ts +81 -0
- package/src/server/tools/__tests__/crud.test.ts +411 -0
- package/src/server/tools/__tests__/get-version.test.ts +27 -0
- package/src/server/tools/__tests__/mcp-interface.test.ts +479 -0
- package/src/server/tools/__tests__/query.test.ts +405 -0
- package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
- package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
- package/src/server/tools/configure-linear.ts +373 -0
- package/src/server/tools/create-epic.ts +44 -0
- package/src/server/tools/create-prd.ts +40 -0
- package/src/server/tools/create-task.ts +47 -0
- package/src/server/tools/criteria.ts +50 -0
- package/src/server/tools/delete-entity.ts +76 -0
- package/src/server/tools/dependencies.ts +55 -0
- package/src/server/tools/get-entity.ts +240 -0
- package/src/server/tools/get-linear-url.ts +28 -0
- package/src/server/tools/get-stats.ts +52 -0
- package/src/server/tools/get-version.ts +20 -0
- package/src/server/tools/index.ts +158 -0
- package/src/server/tools/init-project.ts +108 -0
- package/src/server/tools/query-entities.ts +167 -0
- package/src/server/tools/render-status.ts +219 -0
- package/src/server/tools/update-entity.ts +140 -0
- package/src/server/tools/update-status.ts +166 -0
- package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
- package/src/server/utils/logger.ts +9 -0
- package/src/server/utils/mcp-response.ts +254 -0
- package/src/server/utils/status-transitions.ts +160 -0
- package/src/status-line/__tests__/status-line.test.ts +215 -0
- package/src/status-line/index.ts +147 -0
- package/src/utils/__tests__/chalk-import.test.ts +32 -0
- package/src/utils/__tests__/display.test.ts +97 -0
- package/src/utils/__tests__/status-renderer.test.ts +310 -0
- package/src/utils/display.ts +62 -0
- package/src/utils/status-renderer.ts +214 -0
- package/src/version.ts +5 -0
- package/dist/server/index.js +0 -87063
- package/skills/prd-template/SKILL.md +0 -242
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const TEST_DIR = `/tmp/flux-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
|
+
const mockViewer = {
|
|
10
|
+
id: "viewer-123",
|
|
11
|
+
name: "Test User",
|
|
12
|
+
email: "test@example.com",
|
|
13
|
+
};
|
|
14
|
+
const mockTeam = { id: "team-123", name: "Engineering", key: "ENG" };
|
|
15
|
+
const mockTeams = [
|
|
16
|
+
{ id: "team-123", name: "Engineering", key: "ENG" },
|
|
17
|
+
{ id: "team-456", name: "Product", key: "PROD" },
|
|
18
|
+
];
|
|
19
|
+
const mockProject = { id: "proj-123", name: "Flux Project" };
|
|
20
|
+
const mockProjects = [
|
|
21
|
+
{
|
|
22
|
+
id: "proj-123",
|
|
23
|
+
name: "Flux Project",
|
|
24
|
+
description: "Main project",
|
|
25
|
+
state: "started",
|
|
26
|
+
updatedAt: new Date("2024-01-15"),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "proj-456",
|
|
30
|
+
name: "Another Project",
|
|
31
|
+
description: null,
|
|
32
|
+
state: "planned",
|
|
33
|
+
updatedAt: new Date("2024-01-10"),
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
let lastApiKey = "";
|
|
38
|
+
|
|
39
|
+
mock.module("@linear/sdk", () => ({
|
|
40
|
+
LinearClient: class MockLinearClient {
|
|
41
|
+
static ProjectOrderBy = { UpdatedAt: "updatedAt" };
|
|
42
|
+
|
|
43
|
+
constructor(config: { apiKey: string }) {
|
|
44
|
+
lastApiKey = config.apiKey;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get viewer() {
|
|
48
|
+
if (lastApiKey === "invalid-key") {
|
|
49
|
+
return Promise.reject(new Error("Invalid API key"));
|
|
50
|
+
}
|
|
51
|
+
return Promise.resolve(mockViewer);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async teams() {
|
|
55
|
+
return { nodes: mockTeams };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async team(teamId: string) {
|
|
59
|
+
if (teamId === "invalid-team") {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (teamId === "team-view-fail") {
|
|
63
|
+
return { id: "team-view-fail", name: "View Fail Team", key: "VF" };
|
|
64
|
+
}
|
|
65
|
+
const found = mockTeams.find((t) => t.id === teamId);
|
|
66
|
+
return found ?? mockTeam;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async projects(_options?: any) {
|
|
70
|
+
return { nodes: mockProjects };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async project(projectId: string) {
|
|
74
|
+
if (projectId === "invalid-project") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return mockProject;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async createProject(input: { name: string; teamIds: string[] }) {
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
project: { id: "new-proj-123", name: input.name },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async issueLabels(_filter: any) {
|
|
88
|
+
return { nodes: [] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async createIssueLabel(input: { name: string; teamId: string }) {
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
issueLabel: { id: `label-${input.name}`, name: input.name },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async createCustomView(input: { name: string; teamId?: string }) {
|
|
99
|
+
if (input.teamId === "team-view-fail") {
|
|
100
|
+
throw new Error("View creation failed");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
customView: { id: `view-${input.name}`, name: input.name },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async createViewPreferences(input: {
|
|
109
|
+
customViewId: string;
|
|
110
|
+
type: string;
|
|
111
|
+
viewType: string;
|
|
112
|
+
preferences: Record<string, unknown>;
|
|
113
|
+
}) {
|
|
114
|
+
if (input.customViewId === "view-pref-fail") {
|
|
115
|
+
throw new Error("View preferences creation failed");
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
viewPreferences: { id: `pref-${input.customViewId}` },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
import { clearAdapterCache } from "../../adapters/index.js";
|
|
126
|
+
import { config } from "../../config.js";
|
|
127
|
+
import { configureLinearTool } from "../configure-linear.js";
|
|
128
|
+
|
|
129
|
+
describe("configure_linear MCP Tool", () => {
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
config.clearCache();
|
|
132
|
+
clearAdapterCache();
|
|
133
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
134
|
+
delete process.env.LINEAR_API_KEY;
|
|
135
|
+
|
|
136
|
+
mkdirSync(FLUX_DIR, { recursive: true });
|
|
137
|
+
|
|
138
|
+
const projectJsonPath = join(FLUX_DIR, "project.json");
|
|
139
|
+
const initialProject = {
|
|
140
|
+
name: "test-project",
|
|
141
|
+
ref_prefix: "TEST",
|
|
142
|
+
adapter: { type: "local" },
|
|
143
|
+
};
|
|
144
|
+
require("node:fs").writeFileSync(
|
|
145
|
+
projectJsonPath,
|
|
146
|
+
JSON.stringify(initialProject, null, 2),
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
clearAdapterCache();
|
|
152
|
+
config.clearCache();
|
|
153
|
+
delete process.env.LINEAR_API_KEY;
|
|
154
|
+
if (existsSync(TEST_DIR)) {
|
|
155
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("validation", () => {
|
|
160
|
+
test("throws when no API key source available", async () => {
|
|
161
|
+
await expect(
|
|
162
|
+
configureLinearTool.handler({
|
|
163
|
+
teamId: "team-123",
|
|
164
|
+
projectName: "Test Project",
|
|
165
|
+
}),
|
|
166
|
+
).rejects.toThrow("Linear API key not found");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("requires teamId in non-interactive mode", async () => {
|
|
170
|
+
await expect(
|
|
171
|
+
configureLinearTool.handler({
|
|
172
|
+
apiKey: "lin_api_test123",
|
|
173
|
+
projectName: "Test Project",
|
|
174
|
+
}),
|
|
175
|
+
).rejects.toThrow();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("requires projectName or existingProjectId in non-interactive mode", async () => {
|
|
179
|
+
await expect(
|
|
180
|
+
configureLinearTool.handler({
|
|
181
|
+
apiKey: "lin_api_test123",
|
|
182
|
+
teamId: "team-123",
|
|
183
|
+
}),
|
|
184
|
+
).rejects.toThrow();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("rejects invalid API key", async () => {
|
|
188
|
+
await expect(
|
|
189
|
+
configureLinearTool.handler({
|
|
190
|
+
apiKey: "invalid-key",
|
|
191
|
+
teamId: "team-123",
|
|
192
|
+
projectName: "Test Project",
|
|
193
|
+
}),
|
|
194
|
+
).rejects.toThrow("Invalid Linear API key");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("rejects invalid team ID", async () => {
|
|
198
|
+
await expect(
|
|
199
|
+
configureLinearTool.handler({
|
|
200
|
+
apiKey: "lin_api_test123",
|
|
201
|
+
teamId: "invalid-team",
|
|
202
|
+
projectName: "Test Project",
|
|
203
|
+
}),
|
|
204
|
+
).rejects.toThrow("Team invalid-team not found");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("rejects invalid existing project ID", async () => {
|
|
208
|
+
await expect(
|
|
209
|
+
configureLinearTool.handler({
|
|
210
|
+
apiKey: "lin_api_test123",
|
|
211
|
+
teamId: "team-123",
|
|
212
|
+
existingProjectId: "invalid-project",
|
|
213
|
+
}),
|
|
214
|
+
).rejects.toThrow("Project invalid-project not found");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("API key resolution", () => {
|
|
219
|
+
test("uses API key from input parameter", async () => {
|
|
220
|
+
const result = (await configureLinearTool.handler({
|
|
221
|
+
apiKey: "lin_api_from_input",
|
|
222
|
+
teamId: "team-123",
|
|
223
|
+
projectName: "Test Project",
|
|
224
|
+
})) as any;
|
|
225
|
+
|
|
226
|
+
expect(result.success).toBe(true);
|
|
227
|
+
expect(result.api_key_source).toBe("input");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("uses API key from environment variable", async () => {
|
|
231
|
+
process.env.LINEAR_API_KEY = "lin_api_from_env";
|
|
232
|
+
|
|
233
|
+
const result = (await configureLinearTool.handler({
|
|
234
|
+
teamId: "team-123",
|
|
235
|
+
projectName: "Test Project",
|
|
236
|
+
})) as any;
|
|
237
|
+
|
|
238
|
+
expect(result.success).toBe(true);
|
|
239
|
+
expect(result.api_key_source).toBe("environment");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("uses API key from existing config file", async () => {
|
|
243
|
+
const configPath = join(FLUX_DIR, "linear-config.json");
|
|
244
|
+
require("node:fs").writeFileSync(
|
|
245
|
+
configPath,
|
|
246
|
+
JSON.stringify({ apiKey: "lin_api_from_config" }),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const result = (await configureLinearTool.handler({
|
|
250
|
+
teamId: "team-123",
|
|
251
|
+
projectName: "Test Project",
|
|
252
|
+
})) as any;
|
|
253
|
+
|
|
254
|
+
expect(result.success).toBe(true);
|
|
255
|
+
expect(result.api_key_source).toBe("config");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("input parameter takes priority over environment variable", async () => {
|
|
259
|
+
process.env.LINEAR_API_KEY = "lin_api_from_env";
|
|
260
|
+
|
|
261
|
+
const result = (await configureLinearTool.handler({
|
|
262
|
+
apiKey: "lin_api_from_input",
|
|
263
|
+
teamId: "team-123",
|
|
264
|
+
projectName: "Test Project",
|
|
265
|
+
})) as any;
|
|
266
|
+
|
|
267
|
+
expect(result.success).toBe(true);
|
|
268
|
+
expect(result.api_key_source).toBe("input");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("environment variable takes priority over config file", async () => {
|
|
272
|
+
process.env.LINEAR_API_KEY = "lin_api_from_env";
|
|
273
|
+
const configPath = join(FLUX_DIR, "linear-config.json");
|
|
274
|
+
require("node:fs").writeFileSync(
|
|
275
|
+
configPath,
|
|
276
|
+
JSON.stringify({ apiKey: "lin_api_from_config" }),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const result = (await configureLinearTool.handler({
|
|
280
|
+
teamId: "team-123",
|
|
281
|
+
projectName: "Test Project",
|
|
282
|
+
})) as any;
|
|
283
|
+
|
|
284
|
+
expect(result.success).toBe(true);
|
|
285
|
+
expect(result.api_key_source).toBe("environment");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("config storage", () => {
|
|
290
|
+
test("saves config to .flux/linear-config.json with projectId", async () => {
|
|
291
|
+
await configureLinearTool.handler({
|
|
292
|
+
apiKey: "lin_api_test123",
|
|
293
|
+
teamId: "team-123",
|
|
294
|
+
projectName: "Test Project",
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const configPath = join(FLUX_DIR, "linear-config.json");
|
|
298
|
+
expect(existsSync(configPath)).toBe(true);
|
|
299
|
+
|
|
300
|
+
const savedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
301
|
+
expect(savedConfig.apiKey).toBe("lin_api_test123");
|
|
302
|
+
expect(savedConfig.teamId).toBe("team-123");
|
|
303
|
+
expect(savedConfig.projectId).toBeDefined();
|
|
304
|
+
expect(savedConfig.defaultLabels.prd).toBe("prd");
|
|
305
|
+
expect(savedConfig.defaultLabels.epic).toBe("epic");
|
|
306
|
+
expect(savedConfig.defaultLabels.task).toBe("task");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("saves custom labels", async () => {
|
|
310
|
+
await configureLinearTool.handler({
|
|
311
|
+
apiKey: "lin_api_test123",
|
|
312
|
+
teamId: "team-123",
|
|
313
|
+
projectName: "Test Project",
|
|
314
|
+
prdLabel: "product-requirement",
|
|
315
|
+
epicLabel: "feature",
|
|
316
|
+
taskLabel: "sub-task",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const configPath = join(FLUX_DIR, "linear-config.json");
|
|
320
|
+
const savedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
321
|
+
expect(savedConfig.defaultLabels.prd).toBe("product-requirement");
|
|
322
|
+
expect(savedConfig.defaultLabels.epic).toBe("feature");
|
|
323
|
+
expect(savedConfig.defaultLabels.task).toBe("sub-task");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("uses existing project when existingProjectId provided", async () => {
|
|
327
|
+
await configureLinearTool.handler({
|
|
328
|
+
apiKey: "lin_api_test123",
|
|
329
|
+
teamId: "team-123",
|
|
330
|
+
existingProjectId: "proj-123",
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const configPath = join(FLUX_DIR, "linear-config.json");
|
|
334
|
+
const savedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
335
|
+
expect(savedConfig.projectId).toBe("proj-123");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("project.json update", () => {
|
|
340
|
+
test("updates adapter.type to linear", async () => {
|
|
341
|
+
const projectJsonPath = join(FLUX_DIR, "project.json");
|
|
342
|
+
|
|
343
|
+
await configureLinearTool.handler({
|
|
344
|
+
apiKey: "lin_api_test123",
|
|
345
|
+
teamId: "team-123",
|
|
346
|
+
projectName: "Test Project",
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const updatedProject = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
350
|
+
expect(updatedProject.adapter.type).toBe("linear");
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe("response", () => {
|
|
355
|
+
test("returns success with team and project info", async () => {
|
|
356
|
+
const result = (await configureLinearTool.handler({
|
|
357
|
+
apiKey: "lin_api_test123",
|
|
358
|
+
teamId: "team-123",
|
|
359
|
+
projectName: "My Flux Project",
|
|
360
|
+
})) as any;
|
|
361
|
+
|
|
362
|
+
expect(result.success).toBe(true);
|
|
363
|
+
expect(result.message).toBe("Linear integration configured successfully");
|
|
364
|
+
expect(result.team).toBe("Engineering");
|
|
365
|
+
expect(result.project).toBeDefined();
|
|
366
|
+
expect(result.project.name).toBe("My Flux Project");
|
|
367
|
+
expect(result.labels).toBeDefined();
|
|
368
|
+
expect(result.labels.prd).toBeDefined();
|
|
369
|
+
expect(result.labels.epic).toBeDefined();
|
|
370
|
+
expect(result.labels.task).toBeDefined();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("adapter cache", () => {
|
|
375
|
+
test("clears adapter cache after config change", async () => {
|
|
376
|
+
await configureLinearTool.handler({
|
|
377
|
+
apiKey: "lin_api_test123",
|
|
378
|
+
teamId: "team-123",
|
|
379
|
+
projectName: "Test Project",
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const configPath = join(FLUX_DIR, "linear-config.json");
|
|
383
|
+
expect(existsSync(configPath)).toBe(true);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("view creation", () => {
|
|
388
|
+
test("creates Flux view for new project", async () => {
|
|
389
|
+
const result = (await configureLinearTool.handler({
|
|
390
|
+
apiKey: "lin_api_test123",
|
|
391
|
+
teamId: "team-123",
|
|
392
|
+
projectName: "New Flux Project",
|
|
393
|
+
})) as any;
|
|
394
|
+
|
|
395
|
+
expect(result.view).toBeDefined();
|
|
396
|
+
expect(result.view.created).toBe("Flux: New Flux Project");
|
|
397
|
+
expect(result.view.setup_hint).toContain("Parent issue");
|
|
398
|
+
expect(result.view.skipped).toBeUndefined();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("skips view creation for existing project", async () => {
|
|
402
|
+
const result = (await configureLinearTool.handler({
|
|
403
|
+
apiKey: "lin_api_test123",
|
|
404
|
+
teamId: "team-123",
|
|
405
|
+
existingProjectId: "proj-123",
|
|
406
|
+
})) as any;
|
|
407
|
+
|
|
408
|
+
expect(result.view).toBeDefined();
|
|
409
|
+
expect(result.view.created).toBeNull();
|
|
410
|
+
expect(result.view.skipped).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("succeeds even if view creation fails", async () => {
|
|
414
|
+
const result = (await configureLinearTool.handler({
|
|
415
|
+
apiKey: "lin_api_test123",
|
|
416
|
+
teamId: "team-view-fail",
|
|
417
|
+
projectName: "Test Project",
|
|
418
|
+
})) as any;
|
|
419
|
+
|
|
420
|
+
expect(result.success).toBe(true);
|
|
421
|
+
expect(result.message).toBe("Linear integration configured successfully");
|
|
422
|
+
expect(result.view).toBeDefined();
|
|
423
|
+
expect(result.view.created).toBeNull();
|
|
424
|
+
expect(result.view.error).toBe("View creation failed");
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("interactive mode", () => {
|
|
429
|
+
test("returns teams list when interactive with no teamId", async () => {
|
|
430
|
+
process.env.LINEAR_API_KEY = "lin_api_test123";
|
|
431
|
+
|
|
432
|
+
const result = (await configureLinearTool.handler({
|
|
433
|
+
interactive: true,
|
|
434
|
+
})) as any;
|
|
435
|
+
|
|
436
|
+
expect(result.step).toBe("select_team");
|
|
437
|
+
expect(result.user).toBeDefined();
|
|
438
|
+
expect(result.user.name).toBe("Test User");
|
|
439
|
+
expect(result.user.email).toBe("test@example.com");
|
|
440
|
+
expect(result.api_key_source).toBe("environment");
|
|
441
|
+
expect(result.teams).toBeDefined();
|
|
442
|
+
expect(result.teams).toHaveLength(2);
|
|
443
|
+
expect(result.teams[0].id).toBe("team-123");
|
|
444
|
+
expect(result.teams[0].name).toBe("Engineering");
|
|
445
|
+
expect(result.teams[0].key).toBe("ENG");
|
|
446
|
+
expect(result.teams[1].id).toBe("team-456");
|
|
447
|
+
expect(result.teams[1].name).toBe("Product");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("returns projects list when interactive with teamId but no project", async () => {
|
|
451
|
+
process.env.LINEAR_API_KEY = "lin_api_test123";
|
|
452
|
+
|
|
453
|
+
const result = (await configureLinearTool.handler({
|
|
454
|
+
interactive: true,
|
|
455
|
+
teamId: "team-123",
|
|
456
|
+
})) as any;
|
|
457
|
+
|
|
458
|
+
expect(result.step).toBe("select_project");
|
|
459
|
+
expect(result.user).toBeDefined();
|
|
460
|
+
expect(result.user.name).toBe("Test User");
|
|
461
|
+
expect(result.team).toBeDefined();
|
|
462
|
+
expect(result.team.id).toBe("team-123");
|
|
463
|
+
expect(result.team.name).toBe("Engineering");
|
|
464
|
+
expect(result.team.key).toBe("ENG");
|
|
465
|
+
expect(result.projects).toBeDefined();
|
|
466
|
+
expect(result.projects).toHaveLength(2);
|
|
467
|
+
expect(result.projects[0].id).toBe("proj-123");
|
|
468
|
+
expect(result.projects[0].name).toBe("Flux Project");
|
|
469
|
+
expect(result.projects[0].description).toBe("Main project");
|
|
470
|
+
expect(result.projects[1].id).toBe("proj-456");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("validates team exists in interactive project selection", async () => {
|
|
474
|
+
process.env.LINEAR_API_KEY = "lin_api_test123";
|
|
475
|
+
|
|
476
|
+
await expect(
|
|
477
|
+
configureLinearTool.handler({
|
|
478
|
+
interactive: true,
|
|
479
|
+
teamId: "invalid-team",
|
|
480
|
+
}),
|
|
481
|
+
).rejects.toThrow("Team invalid-team not found");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("proceeds to configuration when interactive with full params", async () => {
|
|
485
|
+
process.env.LINEAR_API_KEY = "lin_api_test123";
|
|
486
|
+
|
|
487
|
+
const result = (await configureLinearTool.handler({
|
|
488
|
+
interactive: true,
|
|
489
|
+
teamId: "team-123",
|
|
490
|
+
existingProjectId: "proj-123",
|
|
491
|
+
})) as any;
|
|
492
|
+
|
|
493
|
+
expect(result.success).toBe(true);
|
|
494
|
+
expect(result.message).toBe("Linear integration configured successfully");
|
|
495
|
+
expect(result.step).toBeUndefined();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("allows new project creation in interactive mode", async () => {
|
|
499
|
+
process.env.LINEAR_API_KEY = "lin_api_test123";
|
|
500
|
+
|
|
501
|
+
const result = (await configureLinearTool.handler({
|
|
502
|
+
interactive: true,
|
|
503
|
+
teamId: "team-123",
|
|
504
|
+
projectName: "New Interactive Project",
|
|
505
|
+
})) as any;
|
|
506
|
+
|
|
507
|
+
expect(result.success).toBe(true);
|
|
508
|
+
expect(result.project.name).toBe("New Interactive Project");
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Set up test environment BEFORE any imports
|
|
6
|
+
const TEST_DIR = `/tmp/flux-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
7
|
+
const FLUX_DIR = join(TEST_DIR, ".flux");
|
|
8
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
9
|
+
|
|
10
|
+
// Now import modules
|
|
11
|
+
import { clearAdapterCache, getAdapter } from "../../adapters/index.js";
|
|
12
|
+
import { config } from "../../config.js";
|
|
13
|
+
import { getLinearUrlTool } from "../get-linear-url.js";
|
|
14
|
+
|
|
15
|
+
describe("get_linear_url MCP Tool", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
config.clearCache();
|
|
18
|
+
clearAdapterCache();
|
|
19
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
20
|
+
|
|
21
|
+
mkdirSync(FLUX_DIR, { recursive: true });
|
|
22
|
+
|
|
23
|
+
// Create Linear config
|
|
24
|
+
const linearConfig = {
|
|
25
|
+
apiKey: "lin_api_test",
|
|
26
|
+
teamId: "team_123",
|
|
27
|
+
projectId: "proj_123",
|
|
28
|
+
defaultLabels: {
|
|
29
|
+
prd: "prd",
|
|
30
|
+
epic: "epic",
|
|
31
|
+
task: "task",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
writeFileSync(
|
|
36
|
+
join(FLUX_DIR, "linear-config.json"),
|
|
37
|
+
JSON.stringify(linearConfig),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
clearAdapterCache();
|
|
43
|
+
config.clearCache();
|
|
44
|
+
if (existsSync(TEST_DIR)) {
|
|
45
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("returns URL for project ref", async () => {
|
|
50
|
+
// Get the adapter and mock its getLinearUrl method
|
|
51
|
+
const adapter = getAdapter() as any;
|
|
52
|
+
adapter.getLinearUrl = mock(async (ref: string) => ({
|
|
53
|
+
url: "https://linear.app/myteam/project/my-project-abc123",
|
|
54
|
+
type: "project",
|
|
55
|
+
name: "My Project",
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const result = (await getLinearUrlTool.handler({
|
|
59
|
+
ref: "proj_abc123",
|
|
60
|
+
})) as any;
|
|
61
|
+
|
|
62
|
+
expect(result.url).toBe(
|
|
63
|
+
"https://linear.app/myteam/project/my-project-abc123",
|
|
64
|
+
);
|
|
65
|
+
expect(result.type).toBe("project");
|
|
66
|
+
expect(result.name).toBe("My Project");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("returns URL for issue identifier", async () => {
|
|
70
|
+
// Get the adapter and mock its getLinearUrl method
|
|
71
|
+
const adapter = getAdapter() as any;
|
|
72
|
+
adapter.getLinearUrl = mock(async (ref: string) => ({
|
|
73
|
+
url: "https://linear.app/myteam/issue/ENG-42",
|
|
74
|
+
type: "issue",
|
|
75
|
+
identifier: "ENG-42",
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
const result = (await getLinearUrlTool.handler({
|
|
79
|
+
ref: "ENG-42",
|
|
80
|
+
})) as any;
|
|
81
|
+
|
|
82
|
+
expect(result.url).toBe("https://linear.app/myteam/issue/ENG-42");
|
|
83
|
+
expect(result.type).toBe("issue");
|
|
84
|
+
expect(result.identifier).toBe("ENG-42");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("throws error if Linear not configured", async () => {
|
|
88
|
+
// Remove Linear config
|
|
89
|
+
rmSync(join(FLUX_DIR, "linear-config.json"));
|
|
90
|
+
clearAdapterCache();
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await getLinearUrlTool.handler({ ref: "ENG-42" });
|
|
94
|
+
expect(true).toBe(false); // Should not reach here
|
|
95
|
+
} catch (err) {
|
|
96
|
+
expect((err as Error).message).toContain("Linear not configured");
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("throws error if ref is empty", async () => {
|
|
101
|
+
try {
|
|
102
|
+
await getLinearUrlTool.handler({ ref: "" });
|
|
103
|
+
expect(true).toBe(false); // Should not reach here
|
|
104
|
+
} catch (err) {
|
|
105
|
+
expect((err as Error).message).toContain("Entity ref is required");
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|