@tenonhq/dovetail-core 0.0.85

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 (78) hide show
  1. package/README.md +6 -0
  2. package/dist/FileLogger.js +207 -0
  3. package/dist/FileUtils.js +387 -0
  4. package/dist/Logger.js +88 -0
  5. package/dist/MultiScopeWatcher.js +615 -0
  6. package/dist/PluginManager.js +125 -0
  7. package/dist/_defaultOptionsOLD.js +44 -0
  8. package/dist/allScopesCommands.js +418 -0
  9. package/dist/appUtils.js +929 -0
  10. package/dist/benchmark.js +104 -0
  11. package/dist/bootstrap.js +62 -0
  12. package/dist/claudeCommand.js +60 -0
  13. package/dist/clickupCommands.js +472 -0
  14. package/dist/clickupPushHelper.js +33 -0
  15. package/dist/commander.js +466 -0
  16. package/dist/commands.js +378 -0
  17. package/dist/config.js +697 -0
  18. package/dist/constants.js +15 -0
  19. package/dist/createRecordCommand.js +314 -0
  20. package/dist/dashboardCommand.js +38 -0
  21. package/dist/defaultOptions.js +14 -0
  22. package/dist/deleteRecordCommand.js +282 -0
  23. package/dist/flowDesigner/values.js +55 -0
  24. package/dist/genericUtils.js +189 -0
  25. package/dist/gitUtils.js +99 -0
  26. package/dist/index.js +35 -0
  27. package/dist/initSystem/corePlugin.js +344 -0
  28. package/dist/initSystem/discovery.js +107 -0
  29. package/dist/initSystem/orchestrator.js +408 -0
  30. package/dist/logMessages.js +82 -0
  31. package/dist/loginCommand.js +20 -0
  32. package/dist/migrateCommand.js +123 -0
  33. package/dist/projectFiles.js +32 -0
  34. package/dist/recentEdits.js +66 -0
  35. package/dist/schemaCommand.js +85 -0
  36. package/dist/snClient.js +519 -0
  37. package/dist/tests/benchmarkRefresh.test.js +265 -0
  38. package/dist/tests/clickupCommands.test.js +384 -0
  39. package/dist/tests/clickupPushHelper.test.js +124 -0
  40. package/dist/tests/configFallback.test.js +68 -0
  41. package/dist/tests/cred.test.js +59 -0
  42. package/dist/tests/discovery.test.js +67 -0
  43. package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
  44. package/dist/tests/errorLogLevels.test.js +273 -0
  45. package/dist/tests/example.test.js +6 -0
  46. package/dist/tests/fileContextSkipReason.test.js +116 -0
  47. package/dist/tests/globalDebounce.test.js +308 -0
  48. package/dist/tests/migrateCommand.test.js +113 -0
  49. package/dist/tests/multi-scope-watcher.test.js +558 -0
  50. package/dist/tests/orchestrator.test.js +23 -0
  51. package/dist/tests/pushFiles.test.js +248 -0
  52. package/dist/tests/rateLimitCoordination.test.js +271 -0
  53. package/dist/tests/resolveConfigReadOnly.test.js +67 -0
  54. package/dist/tests/retryOnHttpErr.test.js +154 -0
  55. package/dist/tests/scopeCaching.test.js +115 -0
  56. package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
  57. package/dist/tests/snClient.fallback.test.js +120 -0
  58. package/dist/tests/syncManifestRefresh.test.js +287 -0
  59. package/dist/tests/syncManifestWhitelist.test.js +192 -0
  60. package/dist/tests/taskClear.test.js +170 -0
  61. package/dist/tests/taskStaleness.test.js +220 -0
  62. package/dist/tests/unwrapSNResponseError.test.js +145 -0
  63. package/dist/tests/v2Values.test.js +79 -0
  64. package/dist/tests/validateTaskId.test.js +304 -0
  65. package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
  66. package/dist/tests/writeEnvVar.test.js +119 -0
  67. package/dist/updateSetCommands.js +702 -0
  68. package/dist/wizard.js +40 -0
  69. package/package.json +69 -0
  70. package/skills/dove-configure-pipeline.md +158 -0
  71. package/skills/dove-create-plugin.md +174 -0
  72. package/skills/dove-create-record.md +135 -0
  73. package/skills/dove-debug-build.md +117 -0
  74. package/skills/dove-delete-record.md +104 -0
  75. package/skills/dove-manage-tables.md +174 -0
  76. package/skills/dove-manage-update-sets.md +88 -0
  77. package/skills/dove-setup-project.md +154 -0
  78. package/skills/dove-troubleshoot-sync.md +113 -0
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ /**
3
+ * Unit tests for the --benchmark flag on `sinc refresh`.
4
+ *
5
+ * Two layers:
6
+ * 1. BenchmarkCollector in isolation — covers p50/p95/max math and the
7
+ * formatSummary string the CLI prints.
8
+ * 2. refreshAllFiles wired to a collector — asserts startScope/endScope
9
+ * bookends the scope and that filesWritten/filesUnchanged match what
10
+ * refresh actually did.
11
+ *
12
+ * The axios interceptor hook (snClient.setBenchmarkSink) is exercised by the
13
+ * real-run integration against workstudio — not here. This suite covers the
14
+ * collector contract and the appUtils lifecycle.
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ const fs = __importStar(require("fs"));
51
+ const os = __importStar(require("os"));
52
+ const path = __importStar(require("path"));
53
+ const benchmark_1 = require("../benchmark");
54
+ // ---------- mocks for refreshAllFiles test (must come before importing) ----
55
+ var mockLogger = {
56
+ info: jest.fn(),
57
+ debug: jest.fn(),
58
+ warn: jest.fn(),
59
+ error: jest.fn(),
60
+ success: jest.fn(),
61
+ getLogLevel: function () { return "warn"; },
62
+ };
63
+ var mockFileLogger = {
64
+ debug: jest.fn(),
65
+ info: jest.fn(),
66
+ warn: jest.fn(),
67
+ error: jest.fn(),
68
+ };
69
+ var mockClient = {
70
+ getManifest: jest.fn(),
71
+ getMissingFiles: jest.fn(),
72
+ };
73
+ jest.mock("../Logger", function () { return { logger: mockLogger }; });
74
+ jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
75
+ jest.mock("../snClient", function () {
76
+ return {
77
+ defaultClient: function () { return mockClient; },
78
+ unwrapSNResponse: function (p) { return Promise.resolve(p).then(function (r) { return r; }); },
79
+ processPushResponse: jest.fn(),
80
+ retryOnErr: jest.fn(),
81
+ retryOnHttpErr: jest.fn(),
82
+ unwrapTableAPIFirstItem: jest.fn(),
83
+ setBenchmarkSink: jest.fn(),
84
+ };
85
+ });
86
+ var mockConfig = {
87
+ getConfig: jest.fn().mockReturnValue({ scopes: { x_cadso_core: {} }, tableOptions: {} }),
88
+ getManifest: jest.fn().mockResolvedValue({
89
+ x_cadso_core: { scope: "x_cadso_core", tables: {} },
90
+ }),
91
+ getSourcePathForScope: jest.fn(),
92
+ getSourcePath: jest.fn(),
93
+ getManifestPath: jest.fn().mockReturnValue("/tmp/dove.manifest.json"),
94
+ resolveConfigForScope: jest.fn().mockImplementation(function () {
95
+ return { tables: ["sys_script_include"], fieldOverrides: {}, apiIncludes: {}, apiExcludes: {} };
96
+ }),
97
+ isMultiScopeManifest: jest.fn().mockReturnValue(true),
98
+ updateManifest: jest.fn(),
99
+ };
100
+ jest.mock("../config", function () { return mockConfig; });
101
+ jest.mock("progress", function () {
102
+ return jest.fn().mockImplementation(function () { return { tick: jest.fn() }; });
103
+ });
104
+ jest.mock("../FileUtils", function () {
105
+ var actual = jest.requireActual("../FileUtils");
106
+ return Object.assign({}, actual, {
107
+ writeScopeManifest: jest.fn().mockResolvedValue(undefined),
108
+ });
109
+ });
110
+ const AppUtils = __importStar(require("../appUtils"));
111
+ // ---------- collector unit tests ----------
112
+ describe("BenchmarkCollector", function () {
113
+ test("formatSummary reports empty state when nothing recorded", function () {
114
+ var collector = new benchmark_1.BenchmarkCollector();
115
+ var summary = collector.formatSummary();
116
+ expect(summary).toContain("Refresh Benchmark");
117
+ expect(summary).toContain("(no samples recorded)");
118
+ });
119
+ test("percentiles reflect the recorded latencies", function () {
120
+ var collector = new benchmark_1.BenchmarkCollector();
121
+ // 10, 20, 30, ..., 100 — p50 at index 5 = 60, p95 at index 9 = 100.
122
+ for (var i = 1; i <= 10; i++) {
123
+ collector.recordHttp({
124
+ path: "/api/test",
125
+ tableCount: 1,
126
+ durationMs: i * 10,
127
+ statusCode: 200,
128
+ responseBytes: 100,
129
+ });
130
+ }
131
+ var summary = collector.formatSummary();
132
+ expect(summary).toContain("p50 60ms");
133
+ expect(summary).toContain("p95 100ms");
134
+ expect(summary).toContain("max 100ms");
135
+ expect(summary).toContain("10 HTTP requests");
136
+ });
137
+ test("scope samples record wall time, request counts, and file counts", function () {
138
+ var collector = new benchmark_1.BenchmarkCollector();
139
+ collector.startScope("x_cadso_core");
140
+ collector.recordHttp({
141
+ path: "/api/bulkDownload",
142
+ tableCount: 3,
143
+ durationMs: 50,
144
+ statusCode: 200,
145
+ responseBytes: 2048,
146
+ });
147
+ collector.recordHttp({
148
+ path: "/api/bulkDownload",
149
+ tableCount: 3,
150
+ durationMs: 80,
151
+ statusCode: 200,
152
+ responseBytes: 4096,
153
+ });
154
+ collector.endScope(7, 42);
155
+ var scopes = collector.getScopeSamples();
156
+ expect(scopes).toHaveLength(1);
157
+ expect(scopes[0].scopeName).toBe("x_cadso_core");
158
+ expect(scopes[0].httpRequests).toBe(2);
159
+ expect(scopes[0].totalResponseBytes).toBe(2048 + 4096);
160
+ expect(scopes[0].filesWritten).toBe(7);
161
+ expect(scopes[0].filesUnchanged).toBe(42);
162
+ expect(scopes[0].wallTimeMs).toBeGreaterThanOrEqual(0);
163
+ var summary = collector.formatSummary();
164
+ expect(summary).toContain("x_cadso_core:");
165
+ expect(summary).toContain("7 written / 42 unchanged");
166
+ });
167
+ test("formatBytes switches units at KB and MB thresholds", function () {
168
+ var collector = new benchmark_1.BenchmarkCollector();
169
+ collector.recordHttp({ path: "/a", tableCount: 1, durationMs: 1, statusCode: 200, responseBytes: 500 });
170
+ expect(collector.formatSummary()).toContain("500B received");
171
+ var collector2 = new benchmark_1.BenchmarkCollector();
172
+ collector2.recordHttp({ path: "/a", tableCount: 1, durationMs: 1, statusCode: 200, responseBytes: 2048 });
173
+ expect(collector2.formatSummary()).toContain("2.0KB received");
174
+ var collector3 = new benchmark_1.BenchmarkCollector();
175
+ collector3.recordHttp({ path: "/a", tableCount: 1, durationMs: 1, statusCode: 200, responseBytes: 2 * 1024 * 1024 });
176
+ expect(collector3.formatSummary()).toContain("2.00MB received");
177
+ });
178
+ test("endScope is a no-op when no scope is active", function () {
179
+ var collector = new benchmark_1.BenchmarkCollector();
180
+ collector.endScope(5, 5);
181
+ expect(collector.getScopeSamples()).toHaveLength(0);
182
+ });
183
+ });
184
+ // ---------- refreshAllFiles integration with collector ----------
185
+ describe("refreshAllFiles — benchmarkCollector lifecycle", function () {
186
+ var tmpRoot;
187
+ beforeEach(function () {
188
+ jest.clearAllMocks();
189
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sinc-bench-test-"));
190
+ mockConfig.getSourcePathForScope.mockReturnValue(tmpRoot);
191
+ mockConfig.getSourcePath.mockReturnValue(tmpRoot);
192
+ });
193
+ afterEach(function () {
194
+ try {
195
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
196
+ }
197
+ catch (e) { }
198
+ });
199
+ test("endScope captures filesWritten and filesUnchanged for a refresh pass", async function () {
200
+ // "Stale" file — content diverges from what the mock returns → will be written.
201
+ fs.mkdirSync(path.join(tmpRoot, "sys_script_include", "StaleRec"), { recursive: true });
202
+ fs.writeFileSync(path.join(tmpRoot, "sys_script_include", "StaleRec", "script.js"), "var x = 1;");
203
+ // "Matching" file — content matches → counted as unchanged.
204
+ fs.mkdirSync(path.join(tmpRoot, "sys_script_include", "SameRec"), { recursive: true });
205
+ var matching = "var same = true;";
206
+ fs.writeFileSync(path.join(tmpRoot, "sys_script_include", "SameRec", "script.js"), matching);
207
+ var manifest = {
208
+ scope: "x_cadso_core",
209
+ tables: {
210
+ sys_script_include: {
211
+ records: {
212
+ StaleRec: { name: "StaleRec", sys_id: "sysid_Stale", files: [{ name: "script", type: "js" }] },
213
+ SameRec: { name: "SameRec", sys_id: "sysid_Same", files: [{ name: "script", type: "js" }] },
214
+ },
215
+ },
216
+ },
217
+ };
218
+ mockClient.getManifest.mockResolvedValue(manifest);
219
+ mockClient.getMissingFiles.mockResolvedValue({
220
+ sys_script_include: {
221
+ records: {
222
+ StaleRec: {
223
+ name: "StaleRec", sys_id: "sysid_Stale",
224
+ files: [{ name: "script", type: "js", content: "var x = 99; // new from instance" }],
225
+ },
226
+ SameRec: {
227
+ name: "SameRec", sys_id: "sysid_Same",
228
+ files: [{ name: "script", type: "js", content: matching }],
229
+ },
230
+ },
231
+ },
232
+ });
233
+ var collector = new benchmark_1.BenchmarkCollector();
234
+ collector.startScope("x_cadso_core");
235
+ await AppUtils.refreshAllFiles(manifest, tmpRoot, {
236
+ benchmarkCollector: collector,
237
+ });
238
+ var scopes = collector.getScopeSamples();
239
+ expect(scopes).toHaveLength(1);
240
+ expect(scopes[0].scopeName).toBe("x_cadso_core");
241
+ // One file had divergent content → written. One matched → unchanged.
242
+ expect(scopes[0].filesWritten).toBe(1);
243
+ expect(scopes[0].filesUnchanged).toBe(1);
244
+ });
245
+ test("endScope(0, 0) on error path so the collector still closes cleanly", async function () {
246
+ mockClient.getMissingFiles.mockRejectedValue(new Error("boom"));
247
+ var manifest = {
248
+ scope: "x_cadso_core",
249
+ tables: {
250
+ sys_script_include: {
251
+ records: {
252
+ Rec: { name: "Rec", sys_id: "sysid_Rec", files: [{ name: "script", type: "js" }] },
253
+ },
254
+ },
255
+ },
256
+ };
257
+ var collector = new benchmark_1.BenchmarkCollector();
258
+ collector.startScope("x_cadso_core");
259
+ await expect(AppUtils.refreshAllFiles(manifest, tmpRoot, { benchmarkCollector: collector })).rejects.toThrow("boom");
260
+ var scopes = collector.getScopeSamples();
261
+ expect(scopes).toHaveLength(1);
262
+ expect(scopes[0].filesWritten).toBe(0);
263
+ expect(scopes[0].filesUnchanged).toBe(0);
264
+ });
265
+ });
@@ -0,0 +1,384 @@
1
+ "use strict";
2
+ // --- Mock setup (must be before imports) ---
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ var mockApi = {
5
+ getAuthorizedUser: jest.fn(),
6
+ getTask: jest.fn(),
7
+ listMyTasks: jest.fn(),
8
+ listTeamTasks: jest.fn(),
9
+ createTask: jest.fn(),
10
+ updateTask: jest.fn(),
11
+ updateTaskStatus: jest.fn(),
12
+ deleteTask: jest.fn(),
13
+ addComment: jest.fn(),
14
+ getTeams: jest.fn(),
15
+ getSpaces: jest.fn(),
16
+ getFolders: jest.fn(),
17
+ getLists: jest.fn(),
18
+ getSpaceLists: jest.fn(),
19
+ getListTasks: jest.fn(),
20
+ };
21
+ jest.mock("@tenonhq/dovetail-clickup", function () {
22
+ return {
23
+ createClickUpApi: jest.fn(function () {
24
+ return mockApi;
25
+ }),
26
+ parseClickUpIdentifier: jest.fn(function (input) {
27
+ return { taskId: input, raw: input };
28
+ }),
29
+ formatForClaude: jest.fn(function () {
30
+ return "## Formatted tasks";
31
+ }),
32
+ formatTaskDetail: jest.fn(function () {
33
+ return "## Task detail";
34
+ }),
35
+ formatTaskSummary: jest.fn(function () {
36
+ return "Task summary";
37
+ }),
38
+ };
39
+ });
40
+ jest.mock("inquirer", function () {
41
+ return {
42
+ default: {
43
+ prompt: jest.fn(),
44
+ },
45
+ prompt: jest.fn(),
46
+ };
47
+ });
48
+ jest.mock("../Logger", function () {
49
+ return {
50
+ logger: {
51
+ info: jest.fn(),
52
+ error: jest.fn(),
53
+ warn: jest.fn(),
54
+ debug: jest.fn(),
55
+ success: jest.fn(),
56
+ },
57
+ };
58
+ });
59
+ jest.mock("../commands", function () {
60
+ return {
61
+ setLogLevel: jest.fn(),
62
+ };
63
+ });
64
+ jest.mock("chalk", function () {
65
+ var identity = function (s) { return s; };
66
+ identity.green = identity;
67
+ identity.bold = identity;
68
+ return {
69
+ default: identity,
70
+ green: identity,
71
+ bold: identity,
72
+ };
73
+ });
74
+ jest.mock("fs", function () {
75
+ var actual = jest.requireActual("fs");
76
+ return Object.assign({}, actual, {
77
+ readFileSync: jest.fn(),
78
+ writeFileSync: jest.fn(),
79
+ existsSync: jest.fn(),
80
+ promises: actual.promises,
81
+ });
82
+ });
83
+ jest.mock("child_process");
84
+ // --- Imports (after mocks) ---
85
+ const clickupCommands_1 = require("../clickupCommands");
86
+ const dovetail_clickup_1 = require("@tenonhq/dovetail-clickup");
87
+ const Logger_1 = require("../Logger");
88
+ // --- Tests ---
89
+ describe("refineUpdateSetName", function () {
90
+ var execSync;
91
+ beforeEach(function () {
92
+ jest.clearAllMocks();
93
+ execSync = require("child_process").execSync;
94
+ });
95
+ it("returns Claude CLI result when valid and under 100 chars", function () {
96
+ execSync.mockReturnValue(" Fix Login Redirect Issue \n");
97
+ var result = (0, clickupCommands_1.refineUpdateSetName)({
98
+ taskName: "Fix login redirect",
99
+ taskId: "abc123",
100
+ taskDescription: "Users are redirected to wrong page",
101
+ });
102
+ expect(result).toBe("Fix Login Redirect Issue");
103
+ });
104
+ it("falls back to cleaned task name when Claude CLI returns empty", function () {
105
+ execSync.mockReturnValue(" \n");
106
+ var result = (0, clickupCommands_1.refineUpdateSetName)({
107
+ taskName: "Fix Bug #42",
108
+ taskId: "abc123",
109
+ taskDescription: "Something broke",
110
+ });
111
+ expect(result).toBe("Fix Bug 42");
112
+ });
113
+ it("falls back when Claude CLI returns string over 100 chars", function () {
114
+ execSync.mockReturnValue("A".repeat(101));
115
+ var result = (0, clickupCommands_1.refineUpdateSetName)({
116
+ taskName: "Short Name",
117
+ taskId: "abc123",
118
+ taskDescription: "",
119
+ });
120
+ expect(result).toBe("Short Name");
121
+ });
122
+ it("falls back when Claude CLI throws (not installed)", function () {
123
+ execSync.mockImplementation(function () {
124
+ throw new Error("command not found: claude");
125
+ });
126
+ var result = (0, clickupCommands_1.refineUpdateSetName)({
127
+ taskName: "Fix Bug",
128
+ taskId: "abc123",
129
+ taskDescription: "",
130
+ });
131
+ expect(result).toBe("Fix Bug");
132
+ });
133
+ it("falls back when Claude CLI times out", function () {
134
+ execSync.mockImplementation(function () {
135
+ var err = new Error("ETIMEDOUT");
136
+ err.killed = true;
137
+ throw err;
138
+ });
139
+ var result = (0, clickupCommands_1.refineUpdateSetName)({
140
+ taskName: "Timeout Task",
141
+ taskId: "abc123",
142
+ taskDescription: "",
143
+ });
144
+ expect(result).toBe("Timeout Task");
145
+ });
146
+ it("strips special characters in fallback", function () {
147
+ execSync.mockImplementation(function () {
148
+ throw new Error("not available");
149
+ });
150
+ var result = (0, clickupCommands_1.refineUpdateSetName)({
151
+ taskName: "Fix: Bug #123 (urgent!)",
152
+ taskId: "abc123",
153
+ taskDescription: "",
154
+ });
155
+ expect(result).toBe("Fix Bug 123 urgent");
156
+ });
157
+ it("truncates fallback to 80 characters", function () {
158
+ execSync.mockImplementation(function () {
159
+ throw new Error("not available");
160
+ });
161
+ var longName = "A".repeat(100);
162
+ var result = (0, clickupCommands_1.refineUpdateSetName)({
163
+ taskName: longName,
164
+ taskId: "abc123",
165
+ taskDescription: "",
166
+ });
167
+ expect(result.length).toBeLessThanOrEqual(80);
168
+ });
169
+ it("sanitizes quotes and backslashes from input before passing to CLI", function () {
170
+ execSync.mockReturnValue("Clean Name");
171
+ (0, clickupCommands_1.refineUpdateSetName)({
172
+ taskName: "Fix 'quotes' and \"double\" and \\backslash",
173
+ taskId: "abc123",
174
+ taskDescription: "Desc with 'quotes' and \\slashes",
175
+ });
176
+ var callArgs = execSync.mock.calls[0][0];
177
+ expect(callArgs).not.toContain("'quotes'");
178
+ expect(callArgs).not.toContain("\\backslash");
179
+ });
180
+ });
181
+ describe("clickupTaskCommand", function () {
182
+ var originalEnv;
183
+ beforeEach(function () {
184
+ jest.clearAllMocks();
185
+ originalEnv = process.env.CLICKUP_API_TOKEN;
186
+ process.env.CLICKUP_API_TOKEN = "pk_test_token";
187
+ });
188
+ afterEach(function () {
189
+ if (originalEnv !== undefined) {
190
+ process.env.CLICKUP_API_TOKEN = originalEnv;
191
+ }
192
+ else {
193
+ delete process.env.CLICKUP_API_TOKEN;
194
+ }
195
+ });
196
+ it("fetches and formats a task on happy path", async function () {
197
+ var mockTask = { id: "t1", name: "Test Task" };
198
+ mockApi.getTask.mockResolvedValue(mockTask);
199
+ var consoleSpy = jest.spyOn(console, "log").mockImplementation();
200
+ await (0, clickupCommands_1.clickupTaskCommand)({ id: "t1", logLevel: "info" });
201
+ expect(dovetail_clickup_1.parseClickUpIdentifier).toHaveBeenCalledWith("t1");
202
+ expect(mockApi.getTask).toHaveBeenCalledWith({ taskId: "t1" });
203
+ expect(dovetail_clickup_1.formatTaskDetail).toHaveBeenCalled();
204
+ expect(consoleSpy).toHaveBeenCalledWith("## Task detail");
205
+ consoleSpy.mockRestore();
206
+ });
207
+ it("throws when CLICKUP_API_TOKEN is missing", async function () {
208
+ delete process.env.CLICKUP_API_TOKEN;
209
+ await expect((0, clickupCommands_1.clickupTaskCommand)({ id: "t1", logLevel: "info" })).rejects.toThrow("CLICKUP_API_TOKEN not set");
210
+ });
211
+ it("logs and re-throws API errors", async function () {
212
+ mockApi.getTask.mockRejectedValue(new Error("API down"));
213
+ await expect((0, clickupCommands_1.clickupTaskCommand)({ id: "t1", logLevel: "info" })).rejects.toThrow("API down");
214
+ expect(Logger_1.logger.error).toHaveBeenCalledWith("Failed to get task");
215
+ });
216
+ });
217
+ describe("clickupTasksCommand", function () {
218
+ var originalEnv;
219
+ beforeEach(function () {
220
+ jest.clearAllMocks();
221
+ originalEnv = process.env.CLICKUP_API_TOKEN;
222
+ process.env.CLICKUP_API_TOKEN = "pk_test_token";
223
+ // Auto-resolve team (single team)
224
+ mockApi.getTeams.mockResolvedValue([{ id: "team1", name: "Tenon" }]);
225
+ });
226
+ afterEach(function () {
227
+ if (originalEnv !== undefined) {
228
+ process.env.CLICKUP_API_TOKEN = originalEnv;
229
+ }
230
+ else {
231
+ delete process.env.CLICKUP_API_TOKEN;
232
+ }
233
+ });
234
+ it("formats and outputs tasks on happy path", async function () {
235
+ mockApi.listMyTasks.mockResolvedValue({
236
+ tasks: [{ id: "t1", name: "Task" }],
237
+ byStatus: { "in progress": [{ id: "t1" }] },
238
+ total: 1,
239
+ });
240
+ var consoleSpy = jest.spyOn(console, "log").mockImplementation();
241
+ await (0, clickupCommands_1.clickupTasksCommand)({ logLevel: "info" });
242
+ expect(mockApi.listMyTasks).toHaveBeenCalled();
243
+ expect(dovetail_clickup_1.formatForClaude).toHaveBeenCalled();
244
+ expect(consoleSpy).toHaveBeenCalledWith("## Formatted tasks");
245
+ consoleSpy.mockRestore();
246
+ });
247
+ it("logs message when no tasks found", async function () {
248
+ mockApi.listMyTasks.mockResolvedValue({
249
+ tasks: [],
250
+ byStatus: {},
251
+ total: 0,
252
+ });
253
+ await (0, clickupCommands_1.clickupTasksCommand)({ logLevel: "info" });
254
+ expect(Logger_1.logger.info).toHaveBeenCalledWith("No tasks assigned to you.");
255
+ });
256
+ it("uses explicit --team flag when provided", async function () {
257
+ mockApi.listMyTasks.mockResolvedValue({ tasks: [], byStatus: {}, total: 0 });
258
+ await (0, clickupCommands_1.clickupTasksCommand)({ team: "explicit-team", logLevel: "info" });
259
+ expect(mockApi.listMyTasks).toHaveBeenCalledWith(expect.objectContaining({ teamId: "explicit-team" }));
260
+ // Should NOT call getTeams when --team is provided
261
+ expect(mockApi.getTeams).not.toHaveBeenCalled();
262
+ });
263
+ it("passes status filter from args", async function () {
264
+ mockApi.listMyTasks.mockResolvedValue({ tasks: [], byStatus: {}, total: 0 });
265
+ await (0, clickupCommands_1.clickupTasksCommand)({
266
+ logLevel: "info",
267
+ status: ["in progress", "blocked"],
268
+ });
269
+ expect(mockApi.listMyTasks).toHaveBeenCalledWith(expect.objectContaining({
270
+ statuses: ["in progress", "blocked"],
271
+ }));
272
+ });
273
+ });
274
+ describe("clickupCommentCommand", function () {
275
+ var originalEnv;
276
+ beforeEach(function () {
277
+ jest.clearAllMocks();
278
+ originalEnv = process.env.CLICKUP_API_TOKEN;
279
+ process.env.CLICKUP_API_TOKEN = "pk_test_token";
280
+ });
281
+ afterEach(function () {
282
+ if (originalEnv !== undefined) {
283
+ process.env.CLICKUP_API_TOKEN = originalEnv;
284
+ }
285
+ else {
286
+ delete process.env.CLICKUP_API_TOKEN;
287
+ }
288
+ });
289
+ it("adds comment to task", async function () {
290
+ mockApi.addComment.mockResolvedValue({});
291
+ await (0, clickupCommands_1.clickupCommentCommand)({ taskId: "t1", msg: "Hello!", logLevel: "info" });
292
+ expect(mockApi.addComment).toHaveBeenCalledWith({
293
+ taskId: "t1",
294
+ commentText: "Hello!",
295
+ });
296
+ });
297
+ it("throws when message is empty", async function () {
298
+ await expect((0, clickupCommands_1.clickupCommentCommand)({ taskId: "t1", msg: "", logLevel: "info" })).rejects.toThrow("Comment message is required");
299
+ });
300
+ it("throws when message is whitespace-only", async function () {
301
+ await expect((0, clickupCommands_1.clickupCommentCommand)({ taskId: "t1", msg: " ", logLevel: "info" })).rejects.toThrow("Comment message is required");
302
+ });
303
+ });
304
+ describe("clickupTeamsCommand", function () {
305
+ var originalEnv;
306
+ beforeEach(function () {
307
+ jest.clearAllMocks();
308
+ originalEnv = process.env.CLICKUP_API_TOKEN;
309
+ process.env.CLICKUP_API_TOKEN = "pk_test_token";
310
+ });
311
+ afterEach(function () {
312
+ if (originalEnv !== undefined) {
313
+ process.env.CLICKUP_API_TOKEN = originalEnv;
314
+ }
315
+ else {
316
+ delete process.env.CLICKUP_API_TOKEN;
317
+ }
318
+ });
319
+ it("lists teams with member counts", async function () {
320
+ mockApi.getTeams.mockResolvedValue([
321
+ { id: "t1", name: "Tenon", members: [{ user: {} }, { user: {} }] },
322
+ ]);
323
+ var consoleSpy = jest.spyOn(console, "log").mockImplementation();
324
+ await (0, clickupCommands_1.clickupTeamsCommand)({ logLevel: "info" });
325
+ expect(mockApi.getTeams).toHaveBeenCalled();
326
+ var output = consoleSpy.mock.calls.map(function (c) { return c[0]; }).join("\n");
327
+ expect(output).toContain("Tenon");
328
+ expect(output).toContain("2 members");
329
+ consoleSpy.mockRestore();
330
+ });
331
+ it("logs message when no teams found", async function () {
332
+ mockApi.getTeams.mockResolvedValue([]);
333
+ await (0, clickupCommands_1.clickupTeamsCommand)({ logLevel: "info" });
334
+ expect(Logger_1.logger.info).toHaveBeenCalledWith("No workspaces found for this token.");
335
+ });
336
+ });
337
+ describe("getClickUpToken (via commands)", function () {
338
+ var originalEnv;
339
+ beforeEach(function () {
340
+ originalEnv = process.env.CLICKUP_API_TOKEN;
341
+ });
342
+ afterEach(function () {
343
+ if (originalEnv !== undefined) {
344
+ process.env.CLICKUP_API_TOKEN = originalEnv;
345
+ }
346
+ else {
347
+ delete process.env.CLICKUP_API_TOKEN;
348
+ }
349
+ });
350
+ it("throws when CLICKUP_API_TOKEN is empty string", async function () {
351
+ process.env.CLICKUP_API_TOKEN = "";
352
+ await expect((0, clickupCommands_1.clickupTaskCommand)({ id: "t1", logLevel: "info" })).rejects.toThrow("CLICKUP_API_TOKEN not set");
353
+ });
354
+ it("throws when CLICKUP_API_TOKEN is undefined", async function () {
355
+ delete process.env.CLICKUP_API_TOKEN;
356
+ await expect((0, clickupCommands_1.clickupTaskCommand)({ id: "t1", logLevel: "info" })).rejects.toThrow("CLICKUP_API_TOKEN not set");
357
+ });
358
+ });
359
+ describe("resolveTeamId (tested via clickupTasksCommand)", function () {
360
+ var originalEnv;
361
+ beforeEach(function () {
362
+ jest.clearAllMocks();
363
+ originalEnv = process.env.CLICKUP_API_TOKEN;
364
+ process.env.CLICKUP_API_TOKEN = "pk_test_token";
365
+ });
366
+ afterEach(function () {
367
+ if (originalEnv !== undefined) {
368
+ process.env.CLICKUP_API_TOKEN = originalEnv;
369
+ }
370
+ else {
371
+ delete process.env.CLICKUP_API_TOKEN;
372
+ }
373
+ });
374
+ it("auto-selects when only one team exists", async function () {
375
+ mockApi.getTeams.mockResolvedValue([{ id: "solo-team", name: "Solo" }]);
376
+ mockApi.listMyTasks.mockResolvedValue({ tasks: [], byStatus: {}, total: 0 });
377
+ await (0, clickupCommands_1.clickupTasksCommand)({ logLevel: "info" });
378
+ expect(mockApi.listMyTasks).toHaveBeenCalledWith(expect.objectContaining({ teamId: "solo-team" }));
379
+ });
380
+ it("throws when no teams found", async function () {
381
+ mockApi.getTeams.mockResolvedValue([]);
382
+ await expect((0, clickupCommands_1.clickupTasksCommand)({ logLevel: "info" })).rejects.toThrow("No ClickUp workspaces found");
383
+ });
384
+ });