@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,91 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+ import type { HydratedIssue } from "../linear/adapter.js";
3
+ import type { LinearConfig } from "../linear/index.js";
4
+
5
+ /**
6
+ * Helper to create a mock HydratedIssue
7
+ */
8
+ function createMockIssue(
9
+ overrides: Partial<HydratedIssue> = {},
10
+ ): HydratedIssue {
11
+ return {
12
+ id: "issue_abc123",
13
+ identifier: "ENG-42",
14
+ title: "Test Issue",
15
+ description: "Test description",
16
+ stateName: "Backlog",
17
+ stateType: "backlog",
18
+ labels: ["epic"],
19
+ parentIdentifier: "ENG-1",
20
+ priority: 3,
21
+ createdAt: new Date("2024-01-01T00:00:00Z"),
22
+ updatedAt: new Date("2024-01-01T00:00:00Z"),
23
+ _raw: {},
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ describe("LinearAdapter", () => {
29
+ const mockConfig: LinearConfig = {
30
+ apiKey: "lin_api_test123",
31
+ teamId: "TEAM-123",
32
+ projectId: "proj_container",
33
+ defaultLabels: {
34
+ prd: "prd",
35
+ epic: "epic",
36
+ task: "task",
37
+ },
38
+ };
39
+
40
+ describe("Instantiation", () => {
41
+ test("can be instantiated with valid config", async () => {
42
+ const { LinearAdapter: LA } = await import("../linear/adapter.js");
43
+ const adapter = new LA(mockConfig);
44
+ expect(adapter).toBeInstanceOf(LA);
45
+ });
46
+ });
47
+
48
+ describe("addCriterion", () => {
49
+ test("adds criterion to epic description", async () => {
50
+ const { LinearAdapter: LA } = await import("../linear/adapter.js");
51
+ const adapter = new LA(mockConfig);
52
+
53
+ const mockEpic = createMockIssue({
54
+ id: "issue_epic_123",
55
+ identifier: "ENG-42",
56
+ title: "Test Epic",
57
+ description: "Epic description",
58
+ labels: ["epic"],
59
+ parentIdentifier: "ENG-1",
60
+ _raw: {
61
+ update: mock(async () => ({})),
62
+ },
63
+ });
64
+
65
+ (adapter as any).fetchIssue = mock(async () => mockEpic);
66
+ (adapter as any).client = {
67
+ execute: mock(async (fn: () => Promise<any>) => fn()),
68
+ };
69
+
70
+ const criterion = await adapter.addCriterion({
71
+ parentRef: "ENG-42",
72
+ criteria: "User can login",
73
+ });
74
+
75
+ expect(criterion.parentId).toBe("ENG-42");
76
+ expect(criterion.criteria).toBe("User can login");
77
+ expect(criterion.isMet).toBe(false);
78
+ });
79
+
80
+ test("throws error if parent not found", async () => {
81
+ const { LinearAdapter: LA } = await import("../linear/adapter.js");
82
+ const adapter = new LA(mockConfig);
83
+
84
+ (adapter as any).fetchIssue = mock(async () => null);
85
+
86
+ await expect(
87
+ adapter.addCriterion({ parentRef: "ENG-999", criteria: "test" }),
88
+ ).rejects.toThrow("Entity not found: ENG-999");
89
+ });
90
+ });
91
+ });
@@ -0,0 +1,425 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+
12
+ describe("Linear Config", () => {
13
+ const originalEnv = process.env.FLUX_PROJECT_ROOT;
14
+ const TEST_DIR = `${realpathSync(tmpdir())}/flux-linear-config-test-${Date.now()}`;
15
+ const FLUX_DIR = `${TEST_DIR}/.flux`;
16
+ const CONFIG_PATH = `${FLUX_DIR}/linear-config.json`;
17
+
18
+ beforeEach(async () => {
19
+ // Clean up any previous test directory
20
+ if (existsSync(TEST_DIR)) {
21
+ rmSync(TEST_DIR, { recursive: true });
22
+ }
23
+
24
+ // Create test directory structure
25
+ mkdirSync(FLUX_DIR, { recursive: true });
26
+
27
+ // Set project root to test directory
28
+ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
29
+
30
+ // Clear the config cache
31
+ const { config } = await import("../../config.js");
32
+ config.clearCache();
33
+ });
34
+
35
+ afterEach(async () => {
36
+ // Restore original env
37
+ if (originalEnv !== undefined) {
38
+ process.env.FLUX_PROJECT_ROOT = originalEnv;
39
+ } else {
40
+ delete process.env.FLUX_PROJECT_ROOT;
41
+ }
42
+
43
+ // Clear the config cache
44
+ const { config } = await import("../../config.js");
45
+ config.clearCache();
46
+
47
+ // Clean up test directory
48
+ if (existsSync(TEST_DIR)) {
49
+ rmSync(TEST_DIR, { recursive: true });
50
+ }
51
+ });
52
+
53
+ describe("linearConfigExists", () => {
54
+ test("returns true when config file exists", async () => {
55
+ writeFileSync(
56
+ CONFIG_PATH,
57
+ JSON.stringify({
58
+ apiKey: "lin_api_test",
59
+ teamId: "TEAM-123",
60
+ projectId: "proj_123",
61
+ }),
62
+ );
63
+
64
+ const { linearConfigExists } = await import("../linear/config.js");
65
+ expect(linearConfigExists()).toBe(true);
66
+ });
67
+
68
+ test("returns false when config file does not exist", async () => {
69
+ const { linearConfigExists } = await import("../linear/config.js");
70
+ expect(linearConfigExists()).toBe(false);
71
+ });
72
+ });
73
+
74
+ describe("validateLinearConfig", () => {
75
+ test("validates valid config with all fields", async () => {
76
+ const validConfig = {
77
+ apiKey: "lin_api_test123",
78
+ teamId: "TEAM-123",
79
+ projectId: "proj_abc123",
80
+ defaultLabels: {
81
+ prd: "prd",
82
+ epic: "epic",
83
+ task: "task",
84
+ },
85
+ };
86
+
87
+ const { validateLinearConfig } = await import("../linear/config.js");
88
+ const result = validateLinearConfig(validConfig);
89
+
90
+ expect(result.apiKey).toBe("lin_api_test123");
91
+ expect(result.teamId).toBe("TEAM-123");
92
+ expect(result.projectId).toBe("proj_abc123");
93
+ expect(result.defaultLabels.prd).toBe("prd");
94
+ expect(result.defaultLabels.epic).toBe("epic");
95
+ expect(result.defaultLabels.task).toBe("task");
96
+ });
97
+
98
+ test("validates valid config with optional defaultLabels omitted", async () => {
99
+ const validConfig = {
100
+ apiKey: "lin_api_test123",
101
+ teamId: "TEAM-123",
102
+ projectId: "proj_abc123",
103
+ };
104
+
105
+ const { validateLinearConfig } = await import("../linear/config.js");
106
+ const result = validateLinearConfig(validConfig);
107
+
108
+ expect(result.apiKey).toBe("lin_api_test123");
109
+ expect(result.teamId).toBe("TEAM-123");
110
+ expect(result.projectId).toBe("proj_abc123");
111
+ expect(result.defaultLabels.prd).toBe("prd");
112
+ expect(result.defaultLabels.epic).toBe("epic");
113
+ expect(result.defaultLabels.task).toBe("task");
114
+ });
115
+
116
+ test("throws error when apiKey is missing", async () => {
117
+ const invalidConfig = {
118
+ teamId: "TEAM-123",
119
+ projectId: "proj_abc123",
120
+ };
121
+
122
+ const { validateLinearConfig } = await import("../linear/config.js");
123
+
124
+ expect(() => validateLinearConfig(invalidConfig)).toThrow(
125
+ "Invalid Linear config: apiKey is required",
126
+ );
127
+ });
128
+
129
+ test("throws error when teamId is missing", async () => {
130
+ const invalidConfig = {
131
+ apiKey: "lin_api_test123",
132
+ projectId: "proj_abc123",
133
+ };
134
+
135
+ const { validateLinearConfig } = await import("../linear/config.js");
136
+
137
+ expect(() => validateLinearConfig(invalidConfig)).toThrow(
138
+ "Invalid Linear config: teamId is required",
139
+ );
140
+ });
141
+
142
+ test("throws error when projectId is missing", async () => {
143
+ const invalidConfig = {
144
+ apiKey: "lin_api_test123",
145
+ teamId: "TEAM-123",
146
+ };
147
+
148
+ const { validateLinearConfig } = await import("../linear/config.js");
149
+
150
+ expect(() => validateLinearConfig(invalidConfig)).toThrow(
151
+ "Invalid Linear config: projectId is required",
152
+ );
153
+ });
154
+
155
+ test("throws error when config is not an object", async () => {
156
+ const { validateLinearConfig } = await import("../linear/config.js");
157
+
158
+ expect(() => validateLinearConfig(null)).toThrow(
159
+ "Invalid Linear config: must be an object",
160
+ );
161
+ expect(() => validateLinearConfig("string")).toThrow(
162
+ "Invalid Linear config: must be an object",
163
+ );
164
+ expect(() => validateLinearConfig(123)).toThrow(
165
+ "Invalid Linear config: must be an object",
166
+ );
167
+ });
168
+
169
+ test("throws error when apiKey is not a string", async () => {
170
+ const invalidConfig = {
171
+ apiKey: 123,
172
+ teamId: "TEAM-123",
173
+ projectId: "proj_abc123",
174
+ };
175
+
176
+ const { validateLinearConfig } = await import("../linear/config.js");
177
+
178
+ expect(() => validateLinearConfig(invalidConfig)).toThrow(
179
+ "Invalid Linear config: apiKey must be a string",
180
+ );
181
+ });
182
+
183
+ test("throws error when teamId is not a string", async () => {
184
+ const invalidConfig = {
185
+ apiKey: "lin_api_test",
186
+ teamId: 123,
187
+ projectId: "proj_abc123",
188
+ };
189
+
190
+ const { validateLinearConfig } = await import("../linear/config.js");
191
+
192
+ expect(() => validateLinearConfig(invalidConfig)).toThrow(
193
+ "Invalid Linear config: teamId must be a string",
194
+ );
195
+ });
196
+
197
+ test("throws error when projectId is not a string", async () => {
198
+ const invalidConfig = {
199
+ apiKey: "lin_api_test",
200
+ teamId: "TEAM-123",
201
+ projectId: 123,
202
+ };
203
+
204
+ const { validateLinearConfig } = await import("../linear/config.js");
205
+
206
+ expect(() => validateLinearConfig(invalidConfig)).toThrow(
207
+ "Invalid Linear config: projectId must be a string",
208
+ );
209
+ });
210
+
211
+ test("uses default labels when defaultLabels is partially provided", async () => {
212
+ const configWithPartialLabels = {
213
+ apiKey: "lin_api_test",
214
+ teamId: "TEAM-123",
215
+ projectId: "proj_abc123",
216
+ defaultLabels: {
217
+ epic: "custom-epic",
218
+ },
219
+ };
220
+
221
+ const { validateLinearConfig } = await import("../linear/config.js");
222
+ const result = validateLinearConfig(configWithPartialLabels);
223
+
224
+ expect(result.defaultLabels.prd).toBe("prd");
225
+ expect(result.defaultLabels.epic).toBe("custom-epic");
226
+ expect(result.defaultLabels.task).toBe("task");
227
+ });
228
+
229
+ test("allows custom prd label", async () => {
230
+ const configWithCustomPrdLabel = {
231
+ apiKey: "lin_api_test",
232
+ teamId: "TEAM-123",
233
+ projectId: "proj_abc123",
234
+ defaultLabels: {
235
+ prd: "custom-prd",
236
+ },
237
+ };
238
+
239
+ const { validateLinearConfig } = await import("../linear/config.js");
240
+ const result = validateLinearConfig(configWithCustomPrdLabel);
241
+
242
+ expect(result.defaultLabels.prd).toBe("custom-prd");
243
+ expect(result.defaultLabels.epic).toBe("epic");
244
+ expect(result.defaultLabels.task).toBe("task");
245
+ });
246
+ });
247
+
248
+ describe("loadLinearConfig", () => {
249
+ test("loads valid config from file", async () => {
250
+ writeFileSync(
251
+ CONFIG_PATH,
252
+ JSON.stringify({
253
+ apiKey: "lin_api_test123",
254
+ teamId: "TEAM-123",
255
+ projectId: "proj_abc123",
256
+ defaultLabels: {
257
+ prd: "prd",
258
+ epic: "epic",
259
+ task: "task",
260
+ },
261
+ }),
262
+ );
263
+
264
+ const { loadLinearConfig } = await import("../linear/config.js");
265
+ const config = loadLinearConfig();
266
+
267
+ expect(config.apiKey).toBe("lin_api_test123");
268
+ expect(config.teamId).toBe("TEAM-123");
269
+ expect(config.projectId).toBe("proj_abc123");
270
+ expect(config.defaultLabels.prd).toBe("prd");
271
+ expect(config.defaultLabels.epic).toBe("epic");
272
+ expect(config.defaultLabels.task).toBe("task");
273
+ });
274
+
275
+ test("loads config with default labels when not provided", async () => {
276
+ writeFileSync(
277
+ CONFIG_PATH,
278
+ JSON.stringify({
279
+ apiKey: "lin_api_test123",
280
+ teamId: "TEAM-123",
281
+ projectId: "proj_abc123",
282
+ }),
283
+ );
284
+
285
+ const { loadLinearConfig } = await import("../linear/config.js");
286
+ const config = loadLinearConfig();
287
+
288
+ expect(config.apiKey).toBe("lin_api_test123");
289
+ expect(config.teamId).toBe("TEAM-123");
290
+ expect(config.projectId).toBe("proj_abc123");
291
+ expect(config.defaultLabels.prd).toBe("prd");
292
+ expect(config.defaultLabels.epic).toBe("epic");
293
+ expect(config.defaultLabels.task).toBe("task");
294
+ });
295
+
296
+ test("throws error when config file does not exist", async () => {
297
+ const { loadLinearConfig } = await import("../linear/config.js");
298
+
299
+ expect(() => loadLinearConfig()).toThrow(
300
+ "Linear config not found. Run configure_linear first.",
301
+ );
302
+ });
303
+
304
+ test("throws error when config file contains invalid JSON", async () => {
305
+ writeFileSync(CONFIG_PATH, "invalid json {");
306
+
307
+ const { loadLinearConfig } = await import("../linear/config.js");
308
+
309
+ expect(() => loadLinearConfig()).toThrow();
310
+ });
311
+
312
+ test("throws error when config is missing required fields", async () => {
313
+ writeFileSync(
314
+ CONFIG_PATH,
315
+ JSON.stringify({
316
+ apiKey: "lin_api_test123",
317
+ // missing teamId and projectId
318
+ }),
319
+ );
320
+
321
+ const { loadLinearConfig } = await import("../linear/config.js");
322
+
323
+ expect(() => loadLinearConfig()).toThrow(
324
+ "Invalid Linear config: teamId is required",
325
+ );
326
+ });
327
+ });
328
+
329
+ describe("saveLinearConfig", () => {
330
+ test("saves valid config to file", async () => {
331
+ const config = {
332
+ apiKey: "lin_api_test123",
333
+ teamId: "TEAM-123",
334
+ projectId: "proj_abc123",
335
+ defaultLabels: {
336
+ prd: "prd",
337
+ epic: "epic",
338
+ task: "task",
339
+ },
340
+ };
341
+
342
+ const { saveLinearConfig } = await import("../linear/config.js");
343
+ saveLinearConfig(config);
344
+
345
+ expect(existsSync(CONFIG_PATH)).toBe(true);
346
+
347
+ const saved = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
348
+ expect(saved.apiKey).toBe("lin_api_test123");
349
+ expect(saved.teamId).toBe("TEAM-123");
350
+ expect(saved.projectId).toBe("proj_abc123");
351
+ expect(saved.defaultLabels.prd).toBe("prd");
352
+ expect(saved.defaultLabels.epic).toBe("epic");
353
+ });
354
+
355
+ test("creates .flux directory if it does not exist", async () => {
356
+ // Remove .flux directory
357
+ rmSync(FLUX_DIR, { recursive: true });
358
+
359
+ const config = {
360
+ apiKey: "lin_api_test123",
361
+ teamId: "TEAM-123",
362
+ projectId: "proj_abc123",
363
+ defaultLabels: {
364
+ prd: "prd",
365
+ epic: "epic",
366
+ task: "task",
367
+ },
368
+ };
369
+
370
+ const { saveLinearConfig } = await import("../linear/config.js");
371
+ saveLinearConfig(config);
372
+
373
+ expect(existsSync(FLUX_DIR)).toBe(true);
374
+ expect(existsSync(CONFIG_PATH)).toBe(true);
375
+ });
376
+
377
+ test("overwrites existing config file", async () => {
378
+ writeFileSync(
379
+ CONFIG_PATH,
380
+ JSON.stringify({
381
+ apiKey: "old_api_key",
382
+ teamId: "OLD-TEAM",
383
+ projectId: "old_proj",
384
+ }),
385
+ );
386
+
387
+ const newConfig = {
388
+ apiKey: "new_api_key",
389
+ teamId: "NEW-TEAM",
390
+ projectId: "new_proj",
391
+ defaultLabels: {
392
+ prd: "custom-prd",
393
+ epic: "custom-epic",
394
+ task: "custom-task",
395
+ },
396
+ };
397
+
398
+ const { saveLinearConfig } = await import("../linear/config.js");
399
+ saveLinearConfig(newConfig);
400
+
401
+ const saved = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
402
+ expect(saved.apiKey).toBe("new_api_key");
403
+ expect(saved.teamId).toBe("NEW-TEAM");
404
+ expect(saved.projectId).toBe("new_proj");
405
+ expect(saved.defaultLabels.prd).toBe("custom-prd");
406
+ expect(saved.defaultLabels.epic).toBe("custom-epic");
407
+ });
408
+
409
+ test("validates config before saving", async () => {
410
+ const invalidConfig = {
411
+ apiKey: "lin_api_test",
412
+ // missing teamId and projectId
413
+ } as any;
414
+
415
+ const { saveLinearConfig } = await import("../linear/config.js");
416
+
417
+ expect(() => saveLinearConfig(invalidConfig)).toThrow(
418
+ "Invalid Linear config: teamId is required",
419
+ );
420
+
421
+ // Ensure file was not created
422
+ expect(existsSync(CONFIG_PATH)).toBe(false);
423
+ });
424
+ });
425
+ });