@cliangdev/flux-plugin 0.0.0-dev.cbdf207 → 0.0.0-dev.df3e9bb

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 (86) hide show
  1. package/README.md +8 -4
  2. package/bin/install.cjs +150 -16
  3. package/package.json +7 -11
  4. package/src/__tests__/version.test.ts +37 -0
  5. package/src/adapters/local/.gitkeep +0 -0
  6. package/src/server/__tests__/config.test.ts +163 -0
  7. package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
  8. package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
  9. package/src/server/adapters/__tests__/dependency-ops.test.ts +395 -0
  10. package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
  11. package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
  12. package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
  13. package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
  14. package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
  15. package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
  16. package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
  17. package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
  18. package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
  19. package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
  20. package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
  21. package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
  22. package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
  23. package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
  24. package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
  25. package/src/server/adapters/factory.ts +90 -0
  26. package/src/server/adapters/index.ts +9 -0
  27. package/src/server/adapters/linear/adapter.ts +1136 -0
  28. package/src/server/adapters/linear/client.ts +169 -0
  29. package/src/server/adapters/linear/config.ts +152 -0
  30. package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
  31. package/src/server/adapters/linear/helpers/index.ts +7 -0
  32. package/src/server/adapters/linear/index.ts +16 -0
  33. package/src/server/adapters/linear/mappers/description.ts +136 -0
  34. package/src/server/adapters/linear/mappers/epic.ts +81 -0
  35. package/src/server/adapters/linear/mappers/index.ts +27 -0
  36. package/src/server/adapters/linear/mappers/prd.ts +178 -0
  37. package/src/server/adapters/linear/mappers/task.ts +82 -0
  38. package/src/server/adapters/linear/types.ts +264 -0
  39. package/src/server/adapters/local-adapter.ts +968 -0
  40. package/src/server/adapters/types.ts +293 -0
  41. package/src/server/config.ts +73 -0
  42. package/src/server/db/__tests__/queries.test.ts +472 -0
  43. package/src/server/db/ids.ts +17 -0
  44. package/src/server/db/index.ts +69 -0
  45. package/src/server/db/queries.ts +142 -0
  46. package/src/server/db/refs.ts +60 -0
  47. package/src/server/db/schema.ts +88 -0
  48. package/src/server/db/sqlite.ts +10 -0
  49. package/src/server/index.ts +83 -0
  50. package/src/server/tools/__tests__/crud.test.ts +301 -0
  51. package/src/server/tools/__tests__/get-version.test.ts +27 -0
  52. package/src/server/tools/__tests__/mcp-interface.test.ts +388 -0
  53. package/src/server/tools/__tests__/query.test.ts +353 -0
  54. package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
  55. package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
  56. package/src/server/tools/configure-linear.ts +373 -0
  57. package/src/server/tools/create-epic.ts +35 -0
  58. package/src/server/tools/create-prd.ts +31 -0
  59. package/src/server/tools/create-task.ts +38 -0
  60. package/src/server/tools/criteria.ts +50 -0
  61. package/src/server/tools/delete-entity.ts +76 -0
  62. package/src/server/tools/dependencies.ts +55 -0
  63. package/src/server/tools/get-entity.ts +238 -0
  64. package/src/server/tools/get-linear-url.ts +28 -0
  65. package/src/server/tools/get-project-context.ts +33 -0
  66. package/src/server/tools/get-stats.ts +52 -0
  67. package/src/server/tools/get-version.ts +20 -0
  68. package/src/server/tools/index.ts +114 -0
  69. package/src/server/tools/init-project.ts +108 -0
  70. package/src/server/tools/query-entities.ts +167 -0
  71. package/src/server/tools/render-status.ts +201 -0
  72. package/src/server/tools/update-entity.ts +140 -0
  73. package/src/server/tools/update-status.ts +166 -0
  74. package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
  75. package/src/server/utils/logger.ts +9 -0
  76. package/src/server/utils/mcp-response.ts +254 -0
  77. package/src/server/utils/status-transitions.ts +160 -0
  78. package/src/status-line/__tests__/status-line.test.ts +215 -0
  79. package/src/status-line/index.ts +147 -0
  80. package/src/utils/__tests__/chalk-import.test.ts +32 -0
  81. package/src/utils/__tests__/display.test.ts +97 -0
  82. package/src/utils/__tests__/status-renderer.test.ts +310 -0
  83. package/src/utils/display.ts +62 -0
  84. package/src/utils/status-renderer.ts +188 -0
  85. package/src/version.ts +5 -0
  86. package/dist/server/index.js +0 -87063
@@ -0,0 +1,60 @@
1
+ import type { Database } from "./sqlite.js";
2
+
3
+ /**
4
+ * Generate a project prefix from project name.
5
+ * Uses acronym for multi-word names, or truncated uppercase for single words.
6
+ *
7
+ * @example
8
+ * generatePrefix("my-saas-app") // -> "MSA"
9
+ * generatePrefix("flux") // -> "FLUX"
10
+ * generatePrefix("my app") // -> "MA"
11
+ */
12
+ export function generatePrefix(projectName: string): string {
13
+ const words = projectName
14
+ .replace(/[^a-zA-Z0-9]/g, " ")
15
+ .split(/\s+/)
16
+ .filter((w) => w.length > 0);
17
+
18
+ if (words.length >= 2) {
19
+ return words
20
+ .map((w) => w[0])
21
+ .join("")
22
+ .toUpperCase()
23
+ .slice(0, 10);
24
+ }
25
+ return projectName
26
+ .replace(/[^a-zA-Z0-9]/g, "")
27
+ .toUpperCase()
28
+ .slice(0, 10);
29
+ }
30
+
31
+ /**
32
+ * Generate a sequential reference for an entity.
33
+ * Refs follow pattern: PREFIX-TYPE{NUMBER}
34
+ *
35
+ * @param db - Database instance
36
+ * @param type - Entity type: "P" (PRD), "E" (Epic), "T" (Task)
37
+ * @param prefix - Project prefix
38
+ * @returns Sequential ref like "MSA-P1", "MSA-E1", "MSA-T1"
39
+ */
40
+ export function generateRef(
41
+ db: Database,
42
+ type: "P" | "E" | "T",
43
+ prefix: string,
44
+ ): string {
45
+ const table = type === "P" ? "prds" : type === "E" ? "epics" : "tasks";
46
+ const pattern = `${prefix}-${type}%`;
47
+ const result = db
48
+ .query(
49
+ `SELECT ref FROM ${table} WHERE ref LIKE ? ORDER BY CAST(SUBSTR(ref, LENGTH(?) + 1) AS INTEGER) DESC LIMIT 1`,
50
+ )
51
+ .get(pattern, `${prefix}-${type}`) as { ref: string } | null;
52
+
53
+ if (!result) {
54
+ return `${prefix}-${type}1`;
55
+ }
56
+
57
+ const match = result.ref.match(new RegExp(`${prefix}-${type}(\\d+)$`));
58
+ const maxNum = match ? parseInt(match[1], 10) : 0;
59
+ return `${prefix}-${type}${maxNum + 1}`;
60
+ }
@@ -0,0 +1,88 @@
1
+ export const SCHEMA = `
2
+ -- Projects table (for multi-project support later)
3
+ CREATE TABLE IF NOT EXISTS projects (
4
+ id TEXT PRIMARY KEY,
5
+ name TEXT NOT NULL,
6
+ ref_prefix TEXT NOT NULL,
7
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
8
+ );
9
+
10
+ -- PRDs
11
+ CREATE TABLE IF NOT EXISTS prds (
12
+ id TEXT PRIMARY KEY,
13
+ project_id TEXT NOT NULL,
14
+ ref TEXT UNIQUE NOT NULL,
15
+ title TEXT NOT NULL,
16
+ description TEXT,
17
+ status TEXT DEFAULT 'DRAFT',
18
+ tag TEXT,
19
+ folder_path TEXT,
20
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
21
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
22
+ FOREIGN KEY (project_id) REFERENCES projects(id)
23
+ );
24
+
25
+ -- Epics
26
+ CREATE TABLE IF NOT EXISTS epics (
27
+ id TEXT PRIMARY KEY,
28
+ prd_id TEXT NOT NULL,
29
+ ref TEXT UNIQUE NOT NULL,
30
+ title TEXT NOT NULL,
31
+ description TEXT,
32
+ status TEXT DEFAULT 'PENDING',
33
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
34
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
35
+ FOREIGN KEY (prd_id) REFERENCES prds(id)
36
+ );
37
+
38
+ -- Tasks
39
+ CREATE TABLE IF NOT EXISTS tasks (
40
+ id TEXT PRIMARY KEY,
41
+ epic_id TEXT NOT NULL,
42
+ ref TEXT UNIQUE NOT NULL,
43
+ title TEXT NOT NULL,
44
+ description TEXT,
45
+ status TEXT DEFAULT 'PENDING',
46
+ priority TEXT DEFAULT 'MEDIUM',
47
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
48
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
49
+ FOREIGN KEY (epic_id) REFERENCES epics(id)
50
+ );
51
+
52
+ -- Acceptance Criteria (for both epics and tasks)
53
+ CREATE TABLE IF NOT EXISTS acceptance_criteria (
54
+ id TEXT PRIMARY KEY,
55
+ parent_type TEXT NOT NULL,
56
+ parent_id TEXT NOT NULL,
57
+ criteria TEXT NOT NULL,
58
+ is_met BOOLEAN DEFAULT FALSE,
59
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
60
+ );
61
+
62
+ -- Epic Dependencies
63
+ CREATE TABLE IF NOT EXISTS epic_dependencies (
64
+ epic_id TEXT NOT NULL,
65
+ depends_on_epic_id TEXT NOT NULL,
66
+ PRIMARY KEY (epic_id, depends_on_epic_id),
67
+ FOREIGN KEY (epic_id) REFERENCES epics(id),
68
+ FOREIGN KEY (depends_on_epic_id) REFERENCES epics(id)
69
+ );
70
+
71
+ -- Task Dependencies
72
+ CREATE TABLE IF NOT EXISTS task_dependencies (
73
+ task_id TEXT NOT NULL,
74
+ depends_on_task_id TEXT NOT NULL,
75
+ PRIMARY KEY (task_id, depends_on_task_id),
76
+ FOREIGN KEY (task_id) REFERENCES tasks(id),
77
+ FOREIGN KEY (depends_on_task_id) REFERENCES tasks(id)
78
+ );
79
+
80
+ -- Indexes for common queries
81
+ CREATE INDEX IF NOT EXISTS idx_prds_project ON prds(project_id);
82
+ CREATE INDEX IF NOT EXISTS idx_prds_status ON prds(status);
83
+ CREATE INDEX IF NOT EXISTS idx_epics_prd ON epics(prd_id);
84
+ CREATE INDEX IF NOT EXISTS idx_epics_status ON epics(status);
85
+ CREATE INDEX IF NOT EXISTS idx_tasks_epic ON tasks(epic_id);
86
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
87
+ CREATE INDEX IF NOT EXISTS idx_criteria_parent ON acceptance_criteria(parent_type, parent_id);
88
+ `;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * SQLite adapter using bun:sqlite.
3
+ * This plugin requires Bun runtime.
4
+ */
5
+
6
+ import type { SQLQueryBindings } from "bun:sqlite";
7
+ import { Database } from "bun:sqlite";
8
+
9
+ export { Database };
10
+ export type { SQLQueryBindings };
@@ -0,0 +1,83 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { config } from "./config.js";
4
+ import { fluxProjectExists, initDb } from "./db/index.js";
5
+ import {
6
+ addCriteriaTool,
7
+ addDependencyTool,
8
+ configureLinearTool,
9
+ createEpicTool,
10
+ createPrdTool,
11
+ createTaskTool,
12
+ deleteEntityTool,
13
+ getEntityTool,
14
+ getProjectContextTool,
15
+ getStatsTool,
16
+ getVersionTool,
17
+ initProjectTool,
18
+ markCriteriaMetTool,
19
+ queryEntitiesTool,
20
+ registerTools,
21
+ removeDependencyTool,
22
+ renderStatusTool,
23
+ type ToolDefinition,
24
+ updateEntityTool,
25
+ updateStatusTool,
26
+ } from "./tools/index.js";
27
+ import { logger } from "./utils/logger.js";
28
+
29
+ const server = new Server(
30
+ { name: "flux", version: "0.1.0" },
31
+ { capabilities: { tools: {} } },
32
+ );
33
+
34
+ // All tools
35
+ const tools: ToolDefinition[] = [
36
+ // CRUD tools
37
+ createPrdTool,
38
+ createEpicTool,
39
+ createTaskTool,
40
+ updateEntityTool,
41
+ deleteEntityTool,
42
+ addDependencyTool,
43
+ removeDependencyTool,
44
+ addCriteriaTool,
45
+ markCriteriaMetTool,
46
+ updateStatusTool,
47
+ // Query tools
48
+ getEntityTool,
49
+ queryEntitiesTool,
50
+ getProjectContextTool,
51
+ initProjectTool,
52
+ getStatsTool,
53
+ getVersionTool,
54
+ // Configuration tools
55
+ configureLinearTool,
56
+ // Display tools
57
+ renderStatusTool,
58
+ ];
59
+
60
+ registerTools(server, tools);
61
+
62
+ async function main() {
63
+ logger.info("Flux MCP server starting...");
64
+ logger.info(` Project root: ${config.projectRoot}`);
65
+ logger.info(` Flux path: ${config.fluxPath}`);
66
+
67
+ // Initialize database if flux project exists
68
+ if (fluxProjectExists()) {
69
+ logger.info("Flux project found, initializing database...");
70
+ initDb();
71
+ } else {
72
+ logger.info("No flux project found. Use init_project to create one.");
73
+ }
74
+
75
+ const transport = new StdioServerTransport();
76
+ await server.connect(transport);
77
+ logger.info("Flux MCP server running");
78
+ }
79
+
80
+ main().catch((err) => {
81
+ logger.error("Server failed to start", err);
82
+ process.exit(1);
83
+ });
@@ -0,0 +1,301 @@
1
+ import { afterEach, beforeEach, describe, expect, 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()}`;
7
+ const FLUX_DIR = join(TEST_DIR, ".flux");
8
+ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
9
+
10
+ // Now import modules
11
+ import { clearAdapterCache } from "../../adapters/index.js";
12
+ import { config } from "../../config.js";
13
+ import { closeDb, initDb } from "../../db/index.js";
14
+ import { createEpicTool } from "../create-epic.js";
15
+ import { createPrdTool } from "../create-prd.js";
16
+ import { createTaskTool } from "../create-task.js";
17
+ import { addCriteriaTool, markCriteriaMetTool } from "../criteria.js";
18
+ import { deleteEntityTool } from "../delete-entity.js";
19
+ import { addDependencyTool, removeDependencyTool } from "../dependencies.js";
20
+ import { updateEntityTool } from "../update-entity.js";
21
+ import { updateStatusTool } from "../update-status.js";
22
+
23
+ describe("CRUD MCP Tools", () => {
24
+ beforeEach(() => {
25
+ config.clearCache();
26
+ clearAdapterCache();
27
+ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
28
+
29
+ mkdirSync(FLUX_DIR, { recursive: true });
30
+ writeFileSync(
31
+ join(FLUX_DIR, "project.json"),
32
+ JSON.stringify({ name: "test-project", ref_prefix: "TEST" }),
33
+ );
34
+ initDb();
35
+ });
36
+
37
+ afterEach(() => {
38
+ closeDb();
39
+ clearAdapterCache();
40
+ config.clearCache();
41
+ if (existsSync(TEST_DIR)) {
42
+ rmSync(TEST_DIR, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ describe("create_prd", () => {
47
+ test("creates PRD with required fields", async () => {
48
+ const result = (await createPrdTool.handler({
49
+ title: "Test PRD",
50
+ })) as any;
51
+ expect(result).toBeDefined();
52
+ expect(result.title).toBe("Test PRD");
53
+ expect(result.ref).toBeDefined();
54
+ expect(result.status).toBe("DRAFT");
55
+ expect(result.id).toBeDefined();
56
+ });
57
+
58
+ test("creates PRD with optional fields", async () => {
59
+ const result = (await createPrdTool.handler({
60
+ title: "Test PRD",
61
+ description: "A description",
62
+ tag: "mvp",
63
+ })) as any;
64
+ expect(result.description).toBe("A description");
65
+ expect(result.tag).toBe("mvp");
66
+ });
67
+ });
68
+
69
+ describe("create_epic", () => {
70
+ test("creates epic linked to PRD", async () => {
71
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
72
+ const epic = (await createEpicTool.handler({
73
+ prd_ref: prd.ref,
74
+ title: "Test Epic",
75
+ })) as any;
76
+
77
+ expect(epic.title).toBe("Test Epic");
78
+ expect(epic.ref).toBeDefined();
79
+ expect(epic.status).toBe("PENDING");
80
+ });
81
+
82
+ test("creates epic with acceptance criteria", async () => {
83
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
84
+ const epic = (await createEpicTool.handler({
85
+ prd_ref: prd.ref,
86
+ title: "Test Epic",
87
+ acceptance_criteria: ["Criterion 1", "Criterion 2"],
88
+ })) as any;
89
+ expect(epic.criteria_count).toBe(2);
90
+ });
91
+
92
+ test("fails with invalid PRD ref", async () => {
93
+ await expect(
94
+ createEpicTool.handler({ prd_ref: "INVALID-P999", title: "Test" }),
95
+ ).rejects.toThrow("PRD not found");
96
+ });
97
+ });
98
+
99
+ describe("create_task", () => {
100
+ test("creates task linked to epic", async () => {
101
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
102
+ const epic = (await createEpicTool.handler({
103
+ prd_ref: prd.ref,
104
+ title: "Test Epic",
105
+ })) as any;
106
+ const task = (await createTaskTool.handler({
107
+ epic_ref: epic.ref,
108
+ title: "Test Task",
109
+ })) as any;
110
+
111
+ expect(task.title).toBe("Test Task");
112
+ expect(task.ref).toBeDefined();
113
+ expect(task.priority).toBe("MEDIUM");
114
+ });
115
+
116
+ test("creates task with priority", async () => {
117
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
118
+ const epic = (await createEpicTool.handler({
119
+ prd_ref: prd.ref,
120
+ title: "Test Epic",
121
+ })) as any;
122
+ const task = (await createTaskTool.handler({
123
+ epic_ref: epic.ref,
124
+ title: "Test Task",
125
+ priority: "HIGH",
126
+ })) as any;
127
+ expect(task.priority).toBe("HIGH");
128
+ });
129
+ });
130
+
131
+ describe("update_entity", () => {
132
+ test("updates PRD fields", async () => {
133
+ const prd = (await createPrdTool.handler({ title: "Original" })) as any;
134
+ const updated = (await updateEntityTool.handler({
135
+ ref: prd.ref,
136
+ fields: { title: "Updated", status: "APPROVED" },
137
+ })) as any;
138
+
139
+ expect(updated.title).toBe("Updated");
140
+ expect(updated.status).toBe("APPROVED");
141
+ });
142
+
143
+ test("updates epic status", async () => {
144
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
145
+ const epic = (await createEpicTool.handler({
146
+ prd_ref: prd.ref,
147
+ title: "Test Epic",
148
+ })) as any;
149
+ const updated = (await updateEntityTool.handler({
150
+ ref: epic.ref,
151
+ fields: { status: "IN_PROGRESS" },
152
+ })) as any;
153
+
154
+ expect(updated.status).toBe("IN_PROGRESS");
155
+ });
156
+ });
157
+
158
+ describe("delete_entity", () => {
159
+ test("deletes task", async () => {
160
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
161
+ const epic = (await createEpicTool.handler({
162
+ prd_ref: prd.ref,
163
+ title: "Test Epic",
164
+ })) as any;
165
+ const task = (await createTaskTool.handler({
166
+ epic_ref: epic.ref,
167
+ title: "Test Task",
168
+ })) as any;
169
+
170
+ const result = (await deleteEntityTool.handler({ ref: task.ref })) as any;
171
+ expect(result.deleted).toBe(task.ref);
172
+ });
173
+
174
+ test("cascade deletes epic with tasks", async () => {
175
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
176
+ const epic = (await createEpicTool.handler({
177
+ prd_ref: prd.ref,
178
+ title: "Test Epic",
179
+ })) as any;
180
+ await createTaskTool.handler({ epic_ref: epic.ref, title: "Task 1" });
181
+ await createTaskTool.handler({ epic_ref: epic.ref, title: "Task 2" });
182
+
183
+ const result = (await deleteEntityTool.handler({ ref: epic.ref })) as any;
184
+ expect(result.cascade.tasks).toBe(2);
185
+ });
186
+ });
187
+
188
+ describe("dependencies", () => {
189
+ test("adds and removes epic dependency", async () => {
190
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
191
+ const epic1 = (await createEpicTool.handler({
192
+ prd_ref: prd.ref,
193
+ title: "Epic 1",
194
+ })) as any;
195
+ const epic2 = (await createEpicTool.handler({
196
+ prd_ref: prd.ref,
197
+ title: "Epic 2",
198
+ })) as any;
199
+
200
+ const addResult = (await addDependencyTool.handler({
201
+ ref: epic2.ref,
202
+ depends_on_ref: epic1.ref,
203
+ })) as any;
204
+ expect(addResult.success).toBe(true);
205
+
206
+ const removeResult = (await removeDependencyTool.handler({
207
+ ref: epic2.ref,
208
+ depends_on_ref: epic1.ref,
209
+ })) as any;
210
+ expect(removeResult.success).toBe(true);
211
+ });
212
+
213
+ test("prevents self-dependency", async () => {
214
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
215
+ const epic = (await createEpicTool.handler({
216
+ prd_ref: prd.ref,
217
+ title: "Epic",
218
+ })) as any;
219
+
220
+ await expect(
221
+ addDependencyTool.handler({ ref: epic.ref, depends_on_ref: epic.ref }),
222
+ ).rejects.toThrow("depend on itself");
223
+ });
224
+ });
225
+
226
+ describe("criteria", () => {
227
+ test("adds and marks criterion", async () => {
228
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
229
+ const epic = (await createEpicTool.handler({
230
+ prd_ref: prd.ref,
231
+ title: "Test Epic",
232
+ })) as any;
233
+
234
+ const criterion = (await addCriteriaTool.handler({
235
+ parent_ref: epic.ref,
236
+ criteria: "Test criterion",
237
+ })) as any;
238
+ expect(criterion.criteria).toBe("Test criterion");
239
+ expect(criterion.is_met).toBe(false);
240
+
241
+ const updated = (await markCriteriaMetTool.handler({
242
+ criteria_id: criterion.id,
243
+ })) as any;
244
+ expect(updated.is_met).toBe(true);
245
+ });
246
+ });
247
+
248
+ describe("update_status", () => {
249
+ test("updates PRD status with valid transition", async () => {
250
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
251
+ // DRAFT → PENDING_REVIEW is a valid transition
252
+ const updated = (await updateStatusTool.handler({
253
+ ref: prd.ref,
254
+ status: "PENDING_REVIEW",
255
+ })) as any;
256
+ expect(updated.status).toBe("PENDING_REVIEW");
257
+ expect(updated.available_transitions).toContain("REVIEWED");
258
+ });
259
+
260
+ test("rejects invalid status for entity type", async () => {
261
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
262
+ await expect(
263
+ updateStatusTool.handler({ ref: prd.ref, status: "IN_PROGRESS" }),
264
+ ).rejects.toThrow("Invalid status");
265
+ });
266
+
267
+ test("rejects invalid status transition", async () => {
268
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
269
+ // DRAFT → APPROVED is not a valid transition (must go through PENDING_REVIEW first)
270
+ await expect(
271
+ updateStatusTool.handler({ ref: prd.ref, status: "APPROVED" }),
272
+ ).rejects.toThrow("Invalid transition");
273
+ });
274
+
275
+ test("updates task status with valid transitions", async () => {
276
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
277
+ const epic = (await createEpicTool.handler({
278
+ prd_ref: prd.ref,
279
+ title: "Test Epic",
280
+ })) as any;
281
+ const task = (await createTaskTool.handler({
282
+ epic_ref: epic.ref,
283
+ title: "Test Task",
284
+ })) as any;
285
+
286
+ // PENDING → IN_PROGRESS is valid
287
+ const inProgress = (await updateStatusTool.handler({
288
+ ref: task.ref,
289
+ status: "IN_PROGRESS",
290
+ })) as any;
291
+ expect(inProgress.status).toBe("IN_PROGRESS");
292
+
293
+ // IN_PROGRESS → COMPLETED is valid
294
+ const completed = (await updateStatusTool.handler({
295
+ ref: task.ref,
296
+ status: "COMPLETED",
297
+ })) as any;
298
+ expect(completed.status).toBe("COMPLETED");
299
+ });
300
+ });
301
+ });
@@ -0,0 +1,27 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { VERSION } from "../../../version.js";
3
+ import { getVersionTool } from "../get-version.js";
4
+
5
+ describe("get_version", () => {
6
+ test("returns version and name", async () => {
7
+ const result = (await getVersionTool.handler({})) as any;
8
+
9
+ expect(result.version).toBe(VERSION);
10
+ expect(result.name).toBe("flux-plugin");
11
+ });
12
+
13
+ test("requires no input parameters", async () => {
14
+ // Should work with empty object
15
+ const result = (await getVersionTool.handler({})) as any;
16
+ expect(result).toBeDefined();
17
+ expect(result.version).toBeDefined();
18
+ expect(result.name).toBeDefined();
19
+ });
20
+
21
+ test("has correct tool definition", () => {
22
+ expect(getVersionTool.name).toBe("get_version");
23
+ expect(getVersionTool.description).toContain("version");
24
+ expect(getVersionTool.inputSchema).toBeDefined();
25
+ expect(getVersionTool.handler).toBeDefined();
26
+ });
27
+ });