@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,558 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ function createMockWatcher() {
40
+ const handlers = {};
41
+ const mock = {
42
+ _handlers: handlers,
43
+ on: jest.fn((event, handler) => {
44
+ if (!handlers[event])
45
+ handlers[event] = [];
46
+ handlers[event].push(handler);
47
+ return mock;
48
+ }),
49
+ close: jest.fn(),
50
+ _emit: (event, ...args) => {
51
+ (handlers[event] || []).forEach((h) => h(...args));
52
+ },
53
+ };
54
+ return mock;
55
+ }
56
+ const mockWatchers = [];
57
+ jest.mock("chokidar", () => ({
58
+ watch: jest.fn(() => {
59
+ const w = createMockWatcher();
60
+ mockWatchers.push(w);
61
+ return w;
62
+ }),
63
+ }));
64
+ // Debounce: capture the global processor, tests trigger manually
65
+ const capturedDebounceFns = [];
66
+ jest.mock("lodash", () => {
67
+ const actual = jest.requireActual("lodash");
68
+ return {
69
+ ...actual,
70
+ debounce: jest.fn((fn) => {
71
+ capturedDebounceFns.push(fn);
72
+ const wrapper = jest.fn();
73
+ wrapper.cancel = jest.fn();
74
+ wrapper.flush = jest.fn(() => fn());
75
+ return wrapper;
76
+ }),
77
+ };
78
+ });
79
+ const mockSNClient = {
80
+ getScopeId: jest.fn(),
81
+ getUserSysId: jest.fn(),
82
+ getCurrentAppUserPrefSysId: jest.fn(),
83
+ updateCurrentAppUserPref: jest.fn(),
84
+ createCurrentAppUserPref: jest.fn(),
85
+ getCurrentUpdateSetUserPref: jest.fn(),
86
+ changeScope: jest.fn().mockResolvedValue(undefined),
87
+ };
88
+ jest.mock("../snClient", () => ({
89
+ defaultClient: jest.fn(() => mockSNClient),
90
+ unwrapSNResponse: jest.fn((val) => val),
91
+ }));
92
+ jest.mock("../FileUtils", () => ({
93
+ getFileContextFromPath: jest.fn(),
94
+ getFileContextWithSkipReason: jest.fn(),
95
+ }));
96
+ jest.mock("../appUtils", () => ({
97
+ groupAppFiles: jest.fn(),
98
+ pushFiles: jest.fn(),
99
+ }));
100
+ jest.mock("../logMessages", () => ({
101
+ logFilePush: jest.fn(),
102
+ }));
103
+ jest.mock("../recentEdits", () => ({
104
+ writeRecentEdit: jest.fn(),
105
+ }));
106
+ jest.mock("../Logger", () => ({
107
+ logger: {
108
+ info: jest.fn(),
109
+ error: jest.fn(),
110
+ warn: jest.fn(),
111
+ debug: jest.fn(),
112
+ success: jest.fn(),
113
+ getLogLevel: jest.fn().mockReturnValue("info"),
114
+ },
115
+ }));
116
+ jest.mock("../config", () => ({
117
+ loadConfigs: jest.fn().mockResolvedValue(undefined),
118
+ getConfig: jest.fn(),
119
+ getRootDir: jest.fn().mockReturnValue("/project"),
120
+ updateManifest: jest.fn(),
121
+ getManifest: jest.fn(),
122
+ getSourcePath: jest.fn().mockReturnValue("/project/src"),
123
+ getScopeManifestPath: jest.fn((scope) => `/project/dove.manifest.${scope}.json`),
124
+ getManifestPath: jest.fn().mockReturnValue("/project/dove.manifest.json"),
125
+ }));
126
+ // Mock fs for manifest loading and config file access
127
+ jest.mock("fs", () => ({
128
+ existsSync: jest.fn(),
129
+ readFileSync: jest.fn(),
130
+ writeFileSync: jest.fn(),
131
+ promises: {
132
+ readFile: jest.fn(),
133
+ writeFile: jest.fn(),
134
+ readdir: jest.fn(),
135
+ mkdir: jest.fn(),
136
+ access: jest.fn(),
137
+ stat: jest.fn(),
138
+ },
139
+ }));
140
+ // Also mock axios for getUpdateSetDetails
141
+ jest.mock("axios", () => ({
142
+ default: {
143
+ create: jest.fn(() => ({
144
+ get: jest.fn().mockResolvedValue({ data: { result: null } }),
145
+ })),
146
+ },
147
+ }));
148
+ // --- Imports ---
149
+ const chokidar_1 = __importDefault(require("chokidar"));
150
+ const fs_1 = __importDefault(require("fs"));
151
+ const ConfigManager = __importStar(require("../config"));
152
+ const FileUtils_1 = require("../FileUtils");
153
+ const appUtils_1 = require("../appUtils");
154
+ const Logger_1 = require("../Logger");
155
+ const MultiScopeWatcher_1 = require("../MultiScopeWatcher");
156
+ // Helper to flush microtask queue (for async code in setInterval callbacks)
157
+ const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
158
+ // --- Fixtures ---
159
+ const makeFileContext = (overrides = {}) => ({
160
+ filePath: "/project/src/x_test_core/sys_script_include/TestScript/script.js",
161
+ ext: ".js",
162
+ sys_id: "abc123",
163
+ name: "TestScript",
164
+ scope: "x_test_core",
165
+ tableName: "sys_script_include",
166
+ targetField: "script",
167
+ ...overrides,
168
+ });
169
+ const MOCK_CONFIG_TWO_SCOPES = {
170
+ sourceDirectory: "src",
171
+ buildDirectory: "build",
172
+ rules: [],
173
+ includes: {},
174
+ excludes: {},
175
+ tableOptions: {},
176
+ refreshInterval: 30,
177
+ scopes: {
178
+ x_test_core: { sourceDirectory: "src/x_test_core" },
179
+ x_test_work: {},
180
+ },
181
+ };
182
+ const MOCK_CONFIG_CUSTOM_DIR = {
183
+ ...MOCK_CONFIG_TWO_SCOPES,
184
+ scopes: {
185
+ x_custom: { sourceDirectory: "custom/path" },
186
+ },
187
+ };
188
+ const MOCK_CONFIG_NO_SCOPES = {
189
+ sourceDirectory: "src",
190
+ };
191
+ // --- Tests ---
192
+ describe("MultiScopeWatcherManager", () => {
193
+ beforeEach(() => {
194
+ jest.clearAllMocks();
195
+ mockWatchers.length = 0;
196
+ capturedDebounceFns.length = 0;
197
+ // Reset the internal state by stopping any previous watchers
198
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
199
+ // Reset scope and user caches (US-013)
200
+ MultiScopeWatcher_1.multiScopeWatcher.cachedScope = null;
201
+ MultiScopeWatcher_1.multiScopeWatcher.cachedUserSysId = null;
202
+ // Reset global debounce state (US-014)
203
+ MultiScopeWatcher_1.multiScopeWatcher.pendingScopes = new Map();
204
+ MultiScopeWatcher_1.multiScopeWatcher.globalProcessQueue = null;
205
+ // Default: scope switching succeeds
206
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
207
+ mockSNClient.getUserSysId.mockResolvedValue([{ sys_id: "user_sys_id" }]);
208
+ mockSNClient.getCurrentAppUserPrefSysId.mockResolvedValue([{ sys_id: "pref_sys_id" }]);
209
+ mockSNClient.updateCurrentAppUserPref.mockResolvedValue({});
210
+ mockSNClient.createCurrentAppUserPref.mockResolvedValue({});
211
+ mockSNClient.getCurrentUpdateSetUserPref.mockResolvedValue([]);
212
+ });
213
+ afterEach(() => {
214
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
215
+ jest.useRealTimers();
216
+ });
217
+ describe("startWatchingAllScopes", () => {
218
+ it("loads config and creates a watcher per scope", async () => {
219
+ ConfigManager.getConfig.mockReturnValue(MOCK_CONFIG_TWO_SCOPES);
220
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
221
+ expect(ConfigManager.loadConfigs).toHaveBeenCalled();
222
+ expect(chokidar_1.default.watch).toHaveBeenCalledTimes(2);
223
+ });
224
+ it("throws when config has no scopes", async () => {
225
+ ConfigManager.getConfig.mockReturnValue(MOCK_CONFIG_NO_SCOPES);
226
+ await expect((0, MultiScopeWatcher_1.startMultiScopeWatching)()).rejects.toThrow("No scopes defined in configuration");
227
+ });
228
+ it("uses custom sourceDirectory from scope config", async () => {
229
+ ConfigManager.getConfig.mockReturnValue(MOCK_CONFIG_CUSTOM_DIR);
230
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
231
+ expect(chokidar_1.default.watch).toHaveBeenCalledWith(expect.stringContaining("custom/path"), expect.any(Object));
232
+ });
233
+ it("uses default src/{scopeName} when no sourceDirectory", async () => {
234
+ ConfigManager.getConfig.mockReturnValue({
235
+ ...MOCK_CONFIG_TWO_SCOPES,
236
+ scopes: { x_default_scope: {} },
237
+ });
238
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
239
+ expect(chokidar_1.default.watch).toHaveBeenCalledWith(expect.stringContaining("/src/x_default_scope"), expect.any(Object));
240
+ });
241
+ it("configures chokidar with correct options", async () => {
242
+ ConfigManager.getConfig.mockReturnValue({
243
+ ...MOCK_CONFIG_TWO_SCOPES,
244
+ scopes: { x_test: {} },
245
+ });
246
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
247
+ expect(chokidar_1.default.watch).toHaveBeenCalledWith(expect.any(String), {
248
+ persistent: true,
249
+ ignoreInitial: true,
250
+ awaitWriteFinish: {
251
+ stabilityThreshold: 300,
252
+ pollInterval: 100,
253
+ },
254
+ });
255
+ });
256
+ it("registers change, add, and error handlers on each watcher", async () => {
257
+ ConfigManager.getConfig.mockReturnValue({
258
+ ...MOCK_CONFIG_TWO_SCOPES,
259
+ scopes: { x_test: {} },
260
+ });
261
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
262
+ const watcher = mockWatchers[0];
263
+ const registeredEvents = watcher.on.mock.calls.map((c) => c[0]);
264
+ expect(registeredEvents).toContain("change");
265
+ expect(registeredEvents).toContain("add");
266
+ expect(registeredEvents).toContain("error");
267
+ });
268
+ });
269
+ describe("file change in scope", () => {
270
+ beforeEach(async () => {
271
+ ConfigManager.getConfig.mockReturnValue({
272
+ ...MOCK_CONFIG_TWO_SCOPES,
273
+ scopes: { x_test_core: { sourceDirectory: "src/x_test_core" } },
274
+ });
275
+ // fs mock for loadScopeManifest
276
+ fs_1.default.existsSync.mockReturnValue(false);
277
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
278
+ });
279
+ it("processes file change through scope switch and push pipeline", async () => {
280
+ const ctx = makeFileContext();
281
+ FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ context: ctx });
282
+ appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
283
+ appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
284
+ // Emit change on the scope's watcher
285
+ mockWatchers[0]._emit("change", ctx.filePath);
286
+ // Trigger the debounced processor for this scope
287
+ await capturedDebounceFns[0]();
288
+ expect(FileUtils_1.getFileContextWithSkipReason).toHaveBeenCalled();
289
+ expect(appUtils_1.groupAppFiles).toHaveBeenCalled();
290
+ expect(appUtils_1.pushFiles).toHaveBeenCalled();
291
+ });
292
+ it("processes file add through the same pipeline", async () => {
293
+ const ctx = makeFileContext();
294
+ FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ context: ctx });
295
+ appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
296
+ appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
297
+ mockWatchers[0]._emit("add", ctx.filePath);
298
+ await capturedDebounceFns[0]();
299
+ expect(appUtils_1.pushFiles).toHaveBeenCalled();
300
+ });
301
+ it("warns when no valid file contexts found", async () => {
302
+ FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ skipReason: "not in manifest" });
303
+ mockWatchers[0]._emit("change", "/project/src/x_test_core/unknown/file.txt");
304
+ await capturedDebounceFns[0]();
305
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No valid file contexts found"));
306
+ expect(appUtils_1.pushFiles).not.toHaveBeenCalled();
307
+ });
308
+ it("logs warning for each skipped file with reason", async () => {
309
+ FileUtils_1.getFileContextWithSkipReason
310
+ .mockReturnValueOnce({ skipReason: "not in manifest" })
311
+ .mockReturnValueOnce({ context: makeFileContext() });
312
+ appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
313
+ appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
314
+ mockWatchers[0]._emit("change", "/project/src/x_test_core/unknown/file.txt");
315
+ mockWatchers[0]._emit("change", makeFileContext().filePath);
316
+ await capturedDebounceFns[0]();
317
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Skipped: /project/src/x_test_core/unknown/file.txt (not in manifest)"));
318
+ });
319
+ it("logs warning with scope not found reason", async () => {
320
+ FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ skipReason: "scope not found" });
321
+ mockWatchers[0]._emit("change", "/project/src/x_unknown/sys_script/file.js");
322
+ await capturedDebounceFns[0]();
323
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Skipped: /project/src/x_unknown/sys_script/file.js (scope not found)"));
324
+ });
325
+ it("logs push summary with skipped file count", async () => {
326
+ var ctx = makeFileContext();
327
+ FileUtils_1.getFileContextWithSkipReason
328
+ .mockReturnValueOnce({ context: ctx })
329
+ .mockReturnValueOnce({ skipReason: "not in manifest" });
330
+ appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
331
+ appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
332
+ mockWatchers[0]._emit("change", ctx.filePath);
333
+ mockWatchers[0]._emit("change", "/project/src/x_test_core/unknown/other.txt");
334
+ await capturedDebounceFns[0]();
335
+ // Should log a summary with pushed/total counts and skipped files
336
+ var allCalls = [
337
+ ...Logger_1.logger.info.mock.calls,
338
+ ...Logger_1.logger.success.mock.calls,
339
+ ].map(function (c) { return c[0]; });
340
+ var summaryCall = allCalls.find(function (msg) {
341
+ return typeof msg === "string" && msg.indexOf("Pushed") !== -1 && msg.indexOf("files to") !== -1;
342
+ });
343
+ expect(summaryCall).toBeDefined();
344
+ expect(summaryCall).toContain("1/2");
345
+ expect(summaryCall).toContain("skipped");
346
+ });
347
+ it("allows file through when scope matches watcher scope", async () => {
348
+ const ctx = makeFileContext({ scope: "x_test_core" });
349
+ FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ context: ctx });
350
+ appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
351
+ appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
352
+ mockWatchers[0]._emit("change", ctx.filePath);
353
+ await capturedDebounceFns[0]();
354
+ expect(appUtils_1.pushFiles).toHaveBeenCalled();
355
+ expect(Logger_1.logger.error).not.toHaveBeenCalledWith(expect.stringContaining("Scope mismatch"));
356
+ });
357
+ it("skips file with error when scope mismatches watcher scope", async () => {
358
+ const ctx = makeFileContext({ scope: "x_other_scope" });
359
+ FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ context: ctx });
360
+ mockWatchers[0]._emit("change", ctx.filePath);
361
+ await capturedDebounceFns[0]();
362
+ // Should log an error about scope mismatch
363
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("Scope mismatch"));
364
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("x_other_scope"));
365
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("x_test_core"));
366
+ // Should not push the file
367
+ expect(appUtils_1.pushFiles).not.toHaveBeenCalled();
368
+ // Should appear in the summary as skipped
369
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No valid file contexts found"));
370
+ });
371
+ it("includes scope-mismatched file in push summary alongside valid files", async () => {
372
+ const validCtx = makeFileContext({ scope: "x_test_core", filePath: "/project/src/x_test_core/sys_script_include/Valid/script.js" });
373
+ const mismatchCtx = makeFileContext({ scope: "x_other_scope", filePath: "/project/src/x_test_core/sys_script_include/Wrong/script.js" });
374
+ FileUtils_1.getFileContextWithSkipReason
375
+ .mockReturnValueOnce({ context: validCtx })
376
+ .mockReturnValueOnce({ context: mismatchCtx });
377
+ appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
378
+ appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
379
+ mockWatchers[0]._emit("change", validCtx.filePath);
380
+ mockWatchers[0]._emit("change", mismatchCtx.filePath);
381
+ await capturedDebounceFns[0]();
382
+ // Valid file should be pushed
383
+ expect(appUtils_1.pushFiles).toHaveBeenCalled();
384
+ // Mismatch should be logged as error
385
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("Scope mismatch"));
386
+ // Push summary should include the skipped file
387
+ var allCalls = [
388
+ ...Logger_1.logger.info.mock.calls,
389
+ ...Logger_1.logger.success.mock.calls,
390
+ ].map(function (c) { return c[0]; });
391
+ var summaryCall = allCalls.find(function (msg) {
392
+ return typeof msg === "string" && msg.indexOf("Pushed") !== -1 && msg.indexOf("files to") !== -1;
393
+ });
394
+ expect(summaryCall).toBeDefined();
395
+ expect(summaryCall).toContain("1/2");
396
+ expect(summaryCall).toContain("skipped");
397
+ });
398
+ it("logs error on watcher error event", () => {
399
+ mockWatchers[0]._emit("error", new Error("FS error"));
400
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("Watcher error: FS error"));
401
+ });
402
+ });
403
+ describe("processScopeQueue - empty queue", () => {
404
+ it("returns early when queue is empty", async () => {
405
+ ConfigManager.getConfig.mockReturnValue({
406
+ ...MOCK_CONFIG_TWO_SCOPES,
407
+ scopes: { x_test_core: {} },
408
+ });
409
+ fs_1.default.existsSync.mockReturnValue(false);
410
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
411
+ // Don't emit any events, just trigger processQueue
412
+ await capturedDebounceFns[0]();
413
+ // switchToScope should NOT have been called
414
+ expect(mockSNClient.getScopeId).not.toHaveBeenCalled();
415
+ expect(FileUtils_1.getFileContextFromPath).not.toHaveBeenCalled();
416
+ });
417
+ });
418
+ describe("switchToScope", () => {
419
+ // switchToScope now uses the Claude REST API changeScope endpoint
420
+ // (gs.setCurrentApplicationId) to switch the active REST session scope.
421
+ // The previous user-preference approach only wrote a DB record and did
422
+ // not affect the session, causing updateRecord() to operate in the wrong scope.
423
+ it("invokes client.changeScope with the target scope", async () => {
424
+ await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_test_core");
425
+ expect(mockSNClient.changeScope).toHaveBeenCalledWith("x_test_core");
426
+ });
427
+ it("propagates errors from changeScope and invalidates cached scope", async () => {
428
+ mockSNClient.changeScope.mockRejectedValueOnce(new Error("changeScope failed"));
429
+ await expect(MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_test_core")).rejects.toThrow("changeScope failed");
430
+ });
431
+ });
432
+ describe("loadScopeManifest", () => {
433
+ it("loads scope-specific manifest when it exists", async () => {
434
+ const manifest = { tables: {}, scope: "x_test_core" };
435
+ fs_1.default.existsSync.mockImplementation((p) => p.includes("dove.manifest.x_test_core.json"));
436
+ fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(manifest));
437
+ await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
438
+ expect(ConfigManager.updateManifest).toHaveBeenCalledWith(manifest);
439
+ });
440
+ it("sets scope field on manifest if missing", async () => {
441
+ const manifest = { tables: {} };
442
+ fs_1.default.existsSync.mockImplementation((p) => p.includes("dove.manifest.x_test_core.json"));
443
+ fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(manifest));
444
+ await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
445
+ expect(ConfigManager.updateManifest).toHaveBeenCalledWith(expect.objectContaining({ scope: "x_test_core" }));
446
+ });
447
+ it("falls back to legacy multi-scope manifest", async () => {
448
+ const legacyManifest = {
449
+ x_test_core: { tables: { sys_script_include: {} } },
450
+ x_test_work: { tables: {} },
451
+ };
452
+ // Scope-specific file doesn't exist, legacy does
453
+ fs_1.default.existsSync.mockImplementation((p) => {
454
+ if (p.includes("dove.manifest.x_test_core.json"))
455
+ return false;
456
+ if (p.includes("dove.manifest.json"))
457
+ return true;
458
+ return false;
459
+ });
460
+ fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(legacyManifest));
461
+ await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
462
+ expect(ConfigManager.updateManifest).toHaveBeenCalledWith(expect.objectContaining({ scope: "x_test_core", tables: { sys_script_include: {} } }));
463
+ });
464
+ it("loads single-scope manifest matching the scope", async () => {
465
+ const manifest = { tables: {}, scope: "x_test_core" };
466
+ fs_1.default.existsSync.mockImplementation((p) => {
467
+ if (p.includes("dove.manifest.x_test_core.json"))
468
+ return false;
469
+ if (p.includes("dove.manifest.json"))
470
+ return true;
471
+ return false;
472
+ });
473
+ fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(manifest));
474
+ await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
475
+ expect(ConfigManager.updateManifest).toHaveBeenCalledWith(manifest);
476
+ });
477
+ it("handles legacy format with tables but no scope field", async () => {
478
+ const manifest = { tables: { sys_script_include: {} } };
479
+ fs_1.default.existsSync.mockImplementation((p) => {
480
+ if (p.includes("dove.manifest.x_test_core.json"))
481
+ return false;
482
+ if (p.includes("dove.manifest.json"))
483
+ return true;
484
+ return false;
485
+ });
486
+ fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(manifest));
487
+ await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
488
+ expect(ConfigManager.updateManifest).toHaveBeenCalledWith(expect.objectContaining({ scope: "x_test_core", tables: { sys_script_include: {} } }));
489
+ });
490
+ it("warns when scope not found in legacy manifest", async () => {
491
+ const manifest = { other_scope: { tables: {} } };
492
+ fs_1.default.existsSync.mockImplementation((p) => {
493
+ if (p.includes("dove.manifest.x_test_core.json"))
494
+ return false;
495
+ if (p.includes("dove.manifest.json"))
496
+ return true;
497
+ return false;
498
+ });
499
+ fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(manifest));
500
+ await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
501
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Scope not found in manifest"));
502
+ });
503
+ it("warns when no manifest files exist", async () => {
504
+ fs_1.default.existsSync.mockReturnValue(false);
505
+ await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
506
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No manifest found"));
507
+ });
508
+ });
509
+ describe("stopWatching", () => {
510
+ it("closes all scope watchers", async () => {
511
+ ConfigManager.getConfig.mockReturnValue(MOCK_CONFIG_TWO_SCOPES);
512
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
513
+ expect(mockWatchers.length).toBe(2);
514
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
515
+ mockWatchers.forEach((w) => {
516
+ expect(w.close).toHaveBeenCalled();
517
+ });
518
+ });
519
+ it("logs stop message", async () => {
520
+ ConfigManager.getConfig.mockReturnValue({
521
+ ...MOCK_CONFIG_TWO_SCOPES,
522
+ scopes: { x_test: {} },
523
+ });
524
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
525
+ jest.clearAllMocks();
526
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
527
+ expect(Logger_1.logger.info).toHaveBeenCalledWith(expect.stringContaining("Stopping watcher"));
528
+ expect(Logger_1.logger.info).toHaveBeenCalledWith("All watchers stopped");
529
+ });
530
+ });
531
+ describe("update set monitoring", () => {
532
+ it("sets up interval with default 120s when no options provided", async () => {
533
+ const setIntervalSpy = jest.spyOn(global, "setInterval");
534
+ ConfigManager.getConfig.mockReturnValue({
535
+ ...MOCK_CONFIG_TWO_SCOPES,
536
+ scopes: { x_test: {} },
537
+ });
538
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
539
+ // startUpdateSetMonitoring runs async — wait for it to set the interval
540
+ await new Promise((r) => setTimeout(r, 50));
541
+ expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 120000);
542
+ setIntervalSpy.mockRestore();
543
+ });
544
+ it("clears interval on stopWatching", async () => {
545
+ const clearIntervalSpy = jest.spyOn(global, "clearInterval");
546
+ ConfigManager.getConfig.mockReturnValue({
547
+ ...MOCK_CONFIG_TWO_SCOPES,
548
+ scopes: { x_test: {} },
549
+ });
550
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
551
+ // Wait for startUpdateSetMonitoring to complete and set the interval
552
+ await new Promise((r) => setTimeout(r, 50));
553
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
554
+ expect(clearIntervalSpy).toHaveBeenCalled();
555
+ clearIntervalSpy.mockRestore();
556
+ });
557
+ });
558
+ });
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const corePlugin_1 = require("../initSystem/corePlugin");
4
+ describe("normalizeInstance", function () {
5
+ it("strips https:// prefix", function () {
6
+ expect((0, corePlugin_1.normalizeInstance)("https://mycompany.service-now.com")).toBe("mycompany.service-now.com");
7
+ });
8
+ it("strips http:// prefix", function () {
9
+ expect((0, corePlugin_1.normalizeInstance)("http://mycompany.service-now.com")).toBe("mycompany.service-now.com");
10
+ });
11
+ it("returns bare hostname without trailing slash", function () {
12
+ expect((0, corePlugin_1.normalizeInstance)("mycompany.service-now.com")).toBe("mycompany.service-now.com");
13
+ });
14
+ it("strips trailing slash if present", function () {
15
+ expect((0, corePlugin_1.normalizeInstance)("mycompany.service-now.com/")).toBe("mycompany.service-now.com");
16
+ });
17
+ it("trims whitespace", function () {
18
+ expect((0, corePlugin_1.normalizeInstance)(" mycompany.service-now.com ")).toBe("mycompany.service-now.com");
19
+ });
20
+ it("handles full URL with protocol and trailing slash", function () {
21
+ expect((0, corePlugin_1.normalizeInstance)("https://mycompany.service-now.com/")).toBe("mycompany.service-now.com");
22
+ });
23
+ });