@cliangdev/flux-plugin 0.1.0-dev.588ae42 → 0.2.0-dev.2b9c207

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.
@@ -194,8 +194,12 @@ describe("LinearAdapter - Dependency Operations", () => {
194
194
  const mockRelation = {
195
195
  id: "relation_1",
196
196
  type: "blocks",
197
- issue: { id: "issue_epic_1", identifier: "ENG-42" },
198
- relatedIssue: { id: "issue_epic_2", identifier: "ENG-43" },
197
+ // Linear SDK returns promises for lazy-loaded related objects
198
+ issue: Promise.resolve({ id: "issue_epic_1", identifier: "ENG-42" }),
199
+ relatedIssue: Promise.resolve({
200
+ id: "issue_epic_2",
201
+ identifier: "ENG-43",
202
+ }),
199
203
  delete: mockDelete,
200
204
  };
201
205
 
@@ -208,7 +212,7 @@ describe("LinearAdapter - Dependency Operations", () => {
208
212
  id: "issue_epic_2",
209
213
  identifier: "ENG-43",
210
214
  _raw: {
211
- relations: mock(async () => ({
215
+ inverseRelations: mock(async () => ({
212
216
  nodes: [mockRelation],
213
217
  })),
214
218
  },
@@ -235,7 +239,7 @@ describe("LinearAdapter - Dependency Operations", () => {
235
239
  id: "issue_epic_1",
236
240
  identifier: "ENG-42",
237
241
  _raw: {
238
- relations: mock(async () => ({ nodes: [] })),
242
+ inverseRelations: mock(async () => ({ nodes: [] })),
239
243
  },
240
244
  });
241
245
 
@@ -265,7 +269,7 @@ describe("LinearAdapter - Dependency Operations", () => {
265
269
  id: "issue_epic_2",
266
270
  identifier: "ENG-43",
267
271
  _raw: {
268
- relations: mock(async () => ({
272
+ inverseRelations: mock(async () => ({
269
273
  nodes: [], // No relations
270
274
  })),
271
275
  },
@@ -294,19 +298,31 @@ describe("LinearAdapter - Dependency Operations", () => {
294
298
  id: "issue_epic_1",
295
299
  identifier: "ENG-43",
296
300
  _raw: {
297
- relations: mock(async () => ({
301
+ inverseRelations: mock(async () => ({
298
302
  nodes: [
299
303
  {
300
304
  id: "relation_1",
301
305
  type: "blocks",
302
- issue: { id: "issue_epic_2", identifier: "ENG-42" },
303
- relatedIssue: { id: "issue_epic_1", identifier: "ENG-43" },
306
+ issue: Promise.resolve({
307
+ id: "issue_epic_2",
308
+ identifier: "ENG-42",
309
+ }),
310
+ relatedIssue: Promise.resolve({
311
+ id: "issue_epic_1",
312
+ identifier: "ENG-43",
313
+ }),
304
314
  },
305
315
  {
306
316
  id: "relation_2",
307
317
  type: "blocks",
308
- issue: { id: "issue_epic_3", identifier: "ENG-44" },
309
- relatedIssue: { id: "issue_epic_1", identifier: "ENG-43" },
318
+ issue: Promise.resolve({
319
+ id: "issue_epic_3",
320
+ identifier: "ENG-44",
321
+ }),
322
+ relatedIssue: Promise.resolve({
323
+ id: "issue_epic_1",
324
+ identifier: "ENG-43",
325
+ }),
310
326
  },
311
327
  ],
312
328
  })),
@@ -328,7 +344,7 @@ describe("LinearAdapter - Dependency Operations", () => {
328
344
  id: "issue_epic_1",
329
345
  identifier: "ENG-42",
330
346
  _raw: {
331
- relations: mock(async () => ({
347
+ inverseRelations: mock(async () => ({
332
348
  nodes: [],
333
349
  })),
334
350
  },
@@ -349,25 +365,43 @@ describe("LinearAdapter - Dependency Operations", () => {
349
365
  id: "issue_epic_1",
350
366
  identifier: "ENG-43",
351
367
  _raw: {
352
- relations: mock(async () => ({
368
+ inverseRelations: mock(async () => ({
353
369
  nodes: [
354
370
  {
355
371
  id: "relation_1",
356
372
  type: "blocks",
357
- issue: { id: "issue_epic_2", identifier: "ENG-42" },
358
- relatedIssue: { id: "issue_epic_1", identifier: "ENG-43" },
373
+ issue: Promise.resolve({
374
+ id: "issue_epic_2",
375
+ identifier: "ENG-42",
376
+ }),
377
+ relatedIssue: Promise.resolve({
378
+ id: "issue_epic_1",
379
+ identifier: "ENG-43",
380
+ }),
359
381
  },
360
382
  {
361
383
  id: "relation_2",
362
384
  type: "duplicate",
363
- issue: { id: "issue_epic_3", identifier: "ENG-44" },
364
- relatedIssue: { id: "issue_epic_1", identifier: "ENG-43" },
385
+ issue: Promise.resolve({
386
+ id: "issue_epic_3",
387
+ identifier: "ENG-44",
388
+ }),
389
+ relatedIssue: Promise.resolve({
390
+ id: "issue_epic_1",
391
+ identifier: "ENG-43",
392
+ }),
365
393
  },
366
394
  {
367
395
  id: "relation_3",
368
396
  type: "related",
369
- issue: { id: "issue_epic_4", identifier: "ENG-45" },
370
- relatedIssue: { id: "issue_epic_1", identifier: "ENG-43" },
397
+ issue: Promise.resolve({
398
+ id: "issue_epic_4",
399
+ identifier: "ENG-45",
400
+ }),
401
+ relatedIssue: Promise.resolve({
402
+ id: "issue_epic_1",
403
+ identifier: "ENG-43",
404
+ }),
371
405
  },
372
406
  ],
373
407
  })),
@@ -887,16 +887,22 @@ export class LinearAdapter implements BackendAdapter {
887
887
  if (!blockedIssue) throw new Error(`Issue not found: ${ref}`);
888
888
  if (!blockerIssue) throw new Error(`Issue not found: ${dependsOnRef}`);
889
889
 
890
+ // Use inverseRelations to find relations where blockedIssue is the target
890
891
  const relations = await this.client.execute<any>(() =>
891
- blockedIssue._raw.relations(),
892
+ blockedIssue._raw.inverseRelations(),
892
893
  );
893
894
 
894
- const relationToDelete = relations.nodes.find(
895
- (rel: any) =>
896
- rel.type === "blocks" &&
897
- rel.issue.id === blockerIssue.id &&
898
- rel.relatedIssue.id === blockedIssue.id,
899
- );
895
+ // Find the relation to delete - need to await rel.issue since it's lazy-loaded
896
+ let relationToDelete: any = null;
897
+ for (const rel of relations.nodes) {
898
+ if (rel.type === "blocks") {
899
+ const relIssue = await rel.issue;
900
+ if (relIssue?.id === blockerIssue.id) {
901
+ relationToDelete = rel;
902
+ break;
903
+ }
904
+ }
905
+ }
900
906
 
901
907
  if (!relationToDelete) {
902
908
  throw new Error(
@@ -911,18 +917,17 @@ export class LinearAdapter implements BackendAdapter {
911
917
  const issue = await this.fetchIssue(ref);
912
918
  if (!issue) throw new Error(`Issue not found: ${ref}`);
913
919
 
920
+ // Use inverseRelations to get relations where this issue is the target (relatedIssueId)
921
+ // This finds issues that block this one
914
922
  const relations = await this.client.execute<any>(() =>
915
- issue._raw.relations(),
923
+ issue._raw.inverseRelations(),
916
924
  );
917
925
 
918
926
  const blockingRefs: string[] = [];
919
927
  for (const rel of relations.nodes) {
920
- if (
921
- rel.type === "blocks" &&
922
- rel.relatedIssue &&
923
- (rel.relatedIssue as any).id === issue.id
924
- ) {
925
- const blockerIssue = rel.issue as any;
928
+ if (rel.type === "blocks") {
929
+ // Linear SDK returns lazy-loaded promises for related objects
930
+ const blockerIssue = await rel.issue;
926
931
  if (blockerIssue?.identifier) {
927
932
  blockingRefs.push(blockerIssue.identifier);
928
933
  }
@@ -312,6 +312,14 @@ export class LocalAdapter implements BackendAdapter {
312
312
  cascade.epics++;
313
313
  }
314
314
 
315
+ // Delete PRD dependencies
316
+ const deletedDeps = db
317
+ .query(
318
+ "DELETE FROM prd_dependencies WHERE prd_id = ? OR depends_on_prd_id = ?",
319
+ )
320
+ .run(prd.id, prd.id);
321
+ cascade.dependencies += deletedDeps.changes;
322
+
315
323
  remove(db, "prds", prd.id);
316
324
 
317
325
  return { deleted: ref, cascade };
@@ -710,7 +718,15 @@ export class LocalAdapter implements BackendAdapter {
710
718
  throw new Error("Dependencies must be between entities of the same type");
711
719
  }
712
720
 
713
- if (entityType === "E") {
721
+ if (entityType === "P") {
722
+ const prd = findByRef<PrdRow>(db, "prds", ref);
723
+ const dependsOnPrd = findByRef<PrdRow>(db, "prds", dependsOnRef);
724
+ if (!prd || !dependsOnPrd) throw new Error("PRD not found");
725
+
726
+ db.query(
727
+ "INSERT OR IGNORE INTO prd_dependencies (prd_id, depends_on_prd_id) VALUES (?, ?)",
728
+ ).run(prd.id, dependsOnPrd.id);
729
+ } else if (entityType === "E") {
714
730
  const epic = findByRef<EpicRow>(db, "epics", ref);
715
731
  const dependsOnEpic = findByRef<EpicRow>(db, "epics", dependsOnRef);
716
732
  if (!epic || !dependsOnEpic) throw new Error("Epic not found");
@@ -726,8 +742,6 @@ export class LocalAdapter implements BackendAdapter {
726
742
  db.query(
727
743
  "INSERT OR IGNORE INTO task_dependencies (task_id, depends_on_task_id) VALUES (?, ?)",
728
744
  ).run(task.id, dependsOnTask.id);
729
- } else {
730
- throw new Error("Dependencies can only be added to Epics or Tasks");
731
745
  }
732
746
  }
733
747
 
@@ -735,7 +749,15 @@ export class LocalAdapter implements BackendAdapter {
735
749
  const db = getDb();
736
750
  const entityType = getEntityType(ref);
737
751
 
738
- if (entityType === "E") {
752
+ if (entityType === "P") {
753
+ const prd = findByRef<PrdRow>(db, "prds", ref);
754
+ const dependsOnPrd = findByRef<PrdRow>(db, "prds", dependsOnRef);
755
+ if (!prd || !dependsOnPrd) throw new Error("PRD not found");
756
+
757
+ db.query(
758
+ "DELETE FROM prd_dependencies WHERE prd_id = ? AND depends_on_prd_id = ?",
759
+ ).run(prd.id, dependsOnPrd.id);
760
+ } else if (entityType === "E") {
739
761
  const epic = findByRef<EpicRow>(db, "epics", ref);
740
762
  const dependsOnEpic = findByRef<EpicRow>(db, "epics", dependsOnRef);
741
763
  if (!epic || !dependsOnEpic) throw new Error("Epic not found");
@@ -758,7 +780,20 @@ export class LocalAdapter implements BackendAdapter {
758
780
  const db = getDb();
759
781
  const entityType = getEntityType(ref);
760
782
 
761
- if (entityType === "E") {
783
+ if (entityType === "P") {
784
+ const prd = findByRef<PrdRow>(db, "prds", ref);
785
+ if (!prd) return [];
786
+
787
+ const deps = db
788
+ .query(
789
+ `SELECT p.ref FROM prd_dependencies pd
790
+ JOIN prds p ON pd.depends_on_prd_id = p.id
791
+ WHERE pd.prd_id = ?`,
792
+ )
793
+ .all(prd.id) as { ref: string }[];
794
+
795
+ return deps.map((d) => d.ref);
796
+ } else if (entityType === "E") {
762
797
  const epic = findByRef<EpicRow>(db, "epics", ref);
763
798
  if (!epic) return [];
764
799
 
@@ -77,6 +77,15 @@ CREATE TABLE IF NOT EXISTS task_dependencies (
77
77
  FOREIGN KEY (depends_on_task_id) REFERENCES tasks(id)
78
78
  );
79
79
 
80
+ -- PRD Dependencies
81
+ CREATE TABLE IF NOT EXISTS prd_dependencies (
82
+ prd_id TEXT NOT NULL,
83
+ depends_on_prd_id TEXT NOT NULL,
84
+ PRIMARY KEY (prd_id, depends_on_prd_id),
85
+ FOREIGN KEY (prd_id) REFERENCES prds(id),
86
+ FOREIGN KEY (depends_on_prd_id) REFERENCES prds(id)
87
+ );
88
+
80
89
  -- Indexes for common queries
81
90
  CREATE INDEX IF NOT EXISTS idx_prds_project ON prds(project_id);
82
91
  CREATE INDEX IF NOT EXISTS idx_prds_status ON prds(status);
@@ -11,7 +11,6 @@ import {
11
11
  createTaskTool,
12
12
  deleteEntityTool,
13
13
  getEntityTool,
14
- getProjectContextTool,
15
14
  getStatsTool,
16
15
  getVersionTool,
17
16
  initProjectTool,
@@ -47,7 +46,6 @@ const tools: ToolDefinition[] = [
47
46
  // Query tools
48
47
  getEntityTool,
49
48
  queryEntitiesTool,
50
- getProjectContextTool,
51
49
  initProjectTool,
52
50
  getStatsTool,
53
51
  getVersionTool,
@@ -53,6 +53,7 @@ describe("CRUD MCP Tools", () => {
53
53
  expect(result.ref).toBeDefined();
54
54
  expect(result.status).toBe("DRAFT");
55
55
  expect(result.id).toBeDefined();
56
+ expect(result.dependencies).toEqual([]);
56
57
  });
57
58
 
58
59
  test("creates PRD with optional fields", async () => {
@@ -64,6 +65,17 @@ describe("CRUD MCP Tools", () => {
64
65
  expect(result.description).toBe("A description");
65
66
  expect(result.tag).toBe("mvp");
66
67
  });
68
+
69
+ test("creates PRD with depends_on", async () => {
70
+ const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
71
+ const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
72
+ const prd3 = (await createPrdTool.handler({
73
+ title: "PRD 3",
74
+ depends_on: [prd1.ref, prd2.ref],
75
+ })) as any;
76
+
77
+ expect(prd3.dependencies).toEqual([prd1.ref, prd2.ref]);
78
+ });
67
79
  });
68
80
 
69
81
  describe("create_epic", () => {
@@ -77,6 +89,7 @@ describe("CRUD MCP Tools", () => {
77
89
  expect(epic.title).toBe("Test Epic");
78
90
  expect(epic.ref).toBeDefined();
79
91
  expect(epic.status).toBe("PENDING");
92
+ expect(epic.dependencies).toEqual([]);
80
93
  });
81
94
 
82
95
  test("creates epic with acceptance criteria", async () => {
@@ -94,6 +107,25 @@ describe("CRUD MCP Tools", () => {
94
107
  createEpicTool.handler({ prd_ref: "INVALID-P999", title: "Test" }),
95
108
  ).rejects.toThrow("PRD not found");
96
109
  });
110
+
111
+ test("creates epic with depends_on", async () => {
112
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
113
+ const epic1 = (await createEpicTool.handler({
114
+ prd_ref: prd.ref,
115
+ title: "Epic 1",
116
+ })) as any;
117
+ const epic2 = (await createEpicTool.handler({
118
+ prd_ref: prd.ref,
119
+ title: "Epic 2",
120
+ })) as any;
121
+ const epic3 = (await createEpicTool.handler({
122
+ prd_ref: prd.ref,
123
+ title: "Epic 3",
124
+ depends_on: [epic1.ref, epic2.ref],
125
+ })) as any;
126
+
127
+ expect(epic3.dependencies).toEqual([epic1.ref, epic2.ref]);
128
+ });
97
129
  });
98
130
 
99
131
  describe("create_task", () => {
@@ -111,6 +143,7 @@ describe("CRUD MCP Tools", () => {
111
143
  expect(task.title).toBe("Test Task");
112
144
  expect(task.ref).toBeDefined();
113
145
  expect(task.priority).toBe("MEDIUM");
146
+ expect(task.dependencies).toEqual([]);
114
147
  });
115
148
 
116
149
  test("creates task with priority", async () => {
@@ -126,6 +159,29 @@ describe("CRUD MCP Tools", () => {
126
159
  })) as any;
127
160
  expect(task.priority).toBe("HIGH");
128
161
  });
162
+
163
+ test("creates task with depends_on", async () => {
164
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
165
+ const epic = (await createEpicTool.handler({
166
+ prd_ref: prd.ref,
167
+ title: "Test Epic",
168
+ })) as any;
169
+ const task1 = (await createTaskTool.handler({
170
+ epic_ref: epic.ref,
171
+ title: "Task 1",
172
+ })) as any;
173
+ const task2 = (await createTaskTool.handler({
174
+ epic_ref: epic.ref,
175
+ title: "Task 2",
176
+ })) as any;
177
+ const task3 = (await createTaskTool.handler({
178
+ epic_ref: epic.ref,
179
+ title: "Task 3",
180
+ depends_on: [task1.ref, task2.ref],
181
+ })) as any;
182
+
183
+ expect(task3.dependencies).toEqual([task1.ref, task2.ref]);
184
+ });
129
185
  });
130
186
 
131
187
  describe("update_entity", () => {
@@ -183,6 +239,19 @@ describe("CRUD MCP Tools", () => {
183
239
  const result = (await deleteEntityTool.handler({ ref: epic.ref })) as any;
184
240
  expect(result.cascade.tasks).toBe(2);
185
241
  });
242
+
243
+ test("cascade deletes PRD with dependencies", async () => {
244
+ const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
245
+ const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
246
+ await addDependencyTool.handler({
247
+ ref: prd2.ref,
248
+ depends_on_ref: prd1.ref,
249
+ });
250
+
251
+ const result = (await deleteEntityTool.handler({ ref: prd1.ref })) as any;
252
+ expect(result.deleted).toBe(prd1.ref);
253
+ expect(result.cascade.dependencies).toBe(1);
254
+ });
186
255
  });
187
256
 
188
257
  describe("dependencies", () => {
@@ -210,6 +279,26 @@ describe("CRUD MCP Tools", () => {
210
279
  expect(removeResult.success).toBe(true);
211
280
  });
212
281
 
282
+ test("adds and removes PRD dependency", async () => {
283
+ const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
284
+ const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
285
+
286
+ const addResult = (await addDependencyTool.handler({
287
+ ref: prd2.ref,
288
+ depends_on_ref: prd1.ref,
289
+ })) as any;
290
+ expect(addResult.success).toBe(true);
291
+ expect(addResult.ref).toBe(prd2.ref);
292
+ expect(addResult.depends_on).toBe(prd1.ref);
293
+
294
+ const removeResult = (await removeDependencyTool.handler({
295
+ ref: prd2.ref,
296
+ depends_on_ref: prd1.ref,
297
+ })) as any;
298
+ expect(removeResult.success).toBe(true);
299
+ expect(removeResult.removed_dependency).toBe(prd1.ref);
300
+ });
301
+
213
302
  test("prevents self-dependency", async () => {
214
303
  const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
215
304
  const epic = (await createEpicTool.handler({
@@ -221,6 +310,26 @@ describe("CRUD MCP Tools", () => {
221
310
  addDependencyTool.handler({ ref: epic.ref, depends_on_ref: epic.ref }),
222
311
  ).rejects.toThrow("depend on itself");
223
312
  });
313
+
314
+ test("prevents PRD self-dependency", async () => {
315
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
316
+
317
+ await expect(
318
+ addDependencyTool.handler({ ref: prd.ref, depends_on_ref: prd.ref }),
319
+ ).rejects.toThrow("depend on itself");
320
+ });
321
+
322
+ test("prevents cross-type dependencies between PRD and Epic", async () => {
323
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
324
+ const epic = (await createEpicTool.handler({
325
+ prd_ref: prd.ref,
326
+ title: "Epic",
327
+ })) as any;
328
+
329
+ await expect(
330
+ addDependencyTool.handler({ ref: prd.ref, depends_on_ref: epic.ref }),
331
+ ).rejects.toThrow("same type");
332
+ });
224
333
  });
225
334
 
226
335
  describe("criteria", () => {
@@ -10,7 +10,12 @@ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
10
10
  import { z } from "zod";
11
11
  import { config } from "../../config.js";
12
12
  import { closeDb, initDb } from "../../db/index.js";
13
- import { createError, registerTools, type ToolDefinition } from "../index.js";
13
+ import {
14
+ createError,
15
+ createProjectNotInitializedError,
16
+ registerTools,
17
+ type ToolDefinition,
18
+ } from "../index.js";
14
19
 
15
20
  describe("MCP Interface", () => {
16
21
  beforeEach(() => {
@@ -44,6 +49,42 @@ describe("MCP Interface", () => {
44
49
  });
45
50
  });
46
51
 
52
+ describe("createProjectNotInitializedError", () => {
53
+ test("creates error with setup instructions", () => {
54
+ const error = createProjectNotInitializedError("/test/cwd", "/test/root");
55
+
56
+ expect(error.error).toBe(true);
57
+ expect(error.code).toBe("PROJECT_NOT_INITIALIZED");
58
+ expect(error.message).toContain("/test/cwd");
59
+ expect(error.message).toContain("/test/root");
60
+ expect(error.setup.instructions).toBeDefined();
61
+ expect(error.setup.options).toHaveLength(2);
62
+ });
63
+
64
+ test("includes command option for interactive setup", () => {
65
+ const error = createProjectNotInitializedError("/cwd", "/root");
66
+ const commandOption = error.setup.options.find(
67
+ (o) => o.method === "command",
68
+ );
69
+
70
+ expect(commandOption).toBeDefined();
71
+ expect(commandOption?.name).toBe("/flux");
72
+ expect(commandOption?.description).toContain("Interactive");
73
+ });
74
+
75
+ test("includes tool option with required params", () => {
76
+ const error = createProjectNotInitializedError("/cwd", "/root");
77
+ const toolOption = error.setup.options.find((o) => o.method === "tool");
78
+
79
+ expect(toolOption).toBeDefined();
80
+ expect(toolOption?.name).toBe("init_project");
81
+ expect(toolOption?.params).toBeDefined();
82
+ expect(toolOption?.params?.name).toBeDefined();
83
+ expect(toolOption?.params?.vision).toBeDefined();
84
+ expect(toolOption?.params?.adapter).toBeDefined();
85
+ });
86
+ });
87
+
47
88
  describe("registerTools", () => {
48
89
  test("sets up list tools handler", () => {
49
90
  const handlers: Record<string, Function> = {};
@@ -287,7 +328,7 @@ describe("Project Validation", () => {
287
328
  }
288
329
  });
289
330
 
290
- test("returns PROJECT_NOT_FOUND for tools requiring project", async () => {
331
+ test("returns PROJECT_NOT_INITIALIZED with setup instructions for tools requiring project", async () => {
291
332
  const handlers: Record<string, Function> = {};
292
333
  const mockServer = {
293
334
  setRequestHandler: mock((schema: any, handler: Function) => {
@@ -317,7 +358,11 @@ describe("Project Validation", () => {
317
358
  expect(result.isError).toBe(true);
318
359
  const parsedError = JSON.parse(result.content[0].text);
319
360
  expect(parsedError.error).toBe(true);
320
- expect(parsedError.code).toBe("PROJECT_NOT_FOUND");
361
+ expect(parsedError.code).toBe("PROJECT_NOT_INITIALIZED");
362
+ expect(parsedError.setup).toBeDefined();
363
+ expect(parsedError.setup.instructions).toBeDefined();
364
+ expect(parsedError.setup.options).toBeDefined();
365
+ expect(parsedError.setup.options.length).toBe(2);
321
366
  });
322
367
 
323
368
  test("allows init_project without existing project", async () => {
@@ -353,7 +398,7 @@ describe("Project Validation", () => {
353
398
  expect(handlerMock).toHaveBeenCalled();
354
399
  });
355
400
 
356
- test("allows get_project_context without existing project", async () => {
401
+ test("allows get_version without existing project", async () => {
357
402
  const handlers: Record<string, Function> = {};
358
403
  const mockServer = {
359
404
  setRequestHandler: mock((schema: any, handler: Function) => {
@@ -363,11 +408,11 @@ describe("Project Validation", () => {
363
408
  }),
364
409
  };
365
410
 
366
- const handlerMock = mock(async () => ({ initialized: false }));
411
+ const handlerMock = mock(async () => ({ version: "1.0.0" }));
367
412
 
368
413
  const testTool: ToolDefinition = {
369
- name: "get_project_context",
370
- description: "Get project context",
414
+ name: "get_version",
415
+ description: "Get version",
371
416
  inputSchema: z.object({}),
372
417
  handler: handlerMock,
373
418
  };
@@ -377,7 +422,7 @@ describe("Project Validation", () => {
377
422
  const callHandler = handlers.call;
378
423
  const result = await callHandler({
379
424
  params: {
380
- name: "get_project_context",
425
+ name: "get_version",
381
426
  arguments: {},
382
427
  },
383
428
  });
@@ -385,4 +430,49 @@ describe("Project Validation", () => {
385
430
  expect(result.isError).toBeUndefined();
386
431
  expect(handlerMock).toHaveBeenCalled();
387
432
  });
433
+
434
+ test("setup options have correct structure", async () => {
435
+ const handlers: Record<string, Function> = {};
436
+ const mockServer = {
437
+ setRequestHandler: mock((schema: any, handler: Function) => {
438
+ if (schema.shape?.method?.value === "tools/call") {
439
+ handlers.call = handler;
440
+ }
441
+ }),
442
+ };
443
+
444
+ const testTool: ToolDefinition = {
445
+ name: "query_entities",
446
+ description: "Query entities",
447
+ inputSchema: z.object({ type: z.string() }),
448
+ handler: async () => ({ items: [] }),
449
+ };
450
+
451
+ registerTools(mockServer as any, [testTool]);
452
+
453
+ const callHandler = handlers.call;
454
+ const result = await callHandler({
455
+ params: {
456
+ name: "query_entities",
457
+ arguments: { type: "prd" },
458
+ },
459
+ });
460
+
461
+ expect(result.isError).toBe(true);
462
+ const parsedError = JSON.parse(result.content[0].text);
463
+
464
+ const commandOption = parsedError.setup.options.find(
465
+ (o: any) => o.method === "command",
466
+ );
467
+ expect(commandOption.name).toBe("/flux");
468
+ expect(commandOption.description).toBeTruthy();
469
+
470
+ const toolOption = parsedError.setup.options.find(
471
+ (o: any) => o.method === "tool",
472
+ );
473
+ expect(toolOption.name).toBe("init_project");
474
+ expect(toolOption.params.name).toBeTruthy();
475
+ expect(toolOption.params.vision).toBeTruthy();
476
+ expect(toolOption.params.adapter).toBeTruthy();
477
+ });
388
478
  });